jfox-cli 1.7.1__tar.gz → 1.7.2__tar.gz

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 (392) hide show
  1. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/CHANGELOG.md +15 -0
  2. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/CLAUDE.md +3 -0
  3. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/PKG-INFO +2 -1
  4. jfox_cli-1.7.2/docs/superpowers/plans/2026-08-16-bm25-concurrent-write-race.md +577 -0
  5. jfox_cli-1.7.2/docs/superpowers/specs/2026-08-16-bm25-concurrent-write-race-design.md +141 -0
  6. jfox_cli-1.7.2/docs/superpowers/specs/2026-08-20-bm25-clear-orphan-followup-design.md +111 -0
  7. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/__init__.py +1 -1
  8. jfox_cli-1.7.2/jfox/bm25_index.py +1012 -0
  9. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/cli.py +10 -0
  10. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/search_engine.py +13 -3
  11. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/pyproject.toml +2 -1
  12. jfox_cli-1.7.2/tests/unit/test_bm25_concurrency.py +576 -0
  13. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_update.py +25 -0
  14. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/uv.lock +3 -1
  15. jfox_cli-1.7.1/jfox/bm25_index.py +0 -613
  16. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/ci/SKILL.md +0 -0
  17. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/release/SKILL.md +0 -0
  18. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/release/release_helper.py +0 -0
  19. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/release-all/SKILL.md +0 -0
  20. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/release-all/release_all_helper.py +0 -0
  21. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/release-cc-plugin/SKILL.md +0 -0
  22. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/release-cc-plugin/release_cc_plugin_helper.py +0 -0
  23. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/release-kimi-plugin/SKILL.md +0 -0
  24. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude/skills/release-kimi-plugin/release_kimi_plugin_helper.py +0 -0
  25. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.claude-plugin/marketplace.json +0 -0
  26. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.githooks/pre-push +0 -0
  27. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  28. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  29. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.github/ISSUE_TEMPLATE/docs_improvement.yml +0 -0
  30. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  31. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.github/workflows/integration-test.yml +0 -0
  33. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.github/workflows/publish.yml +0 -0
  34. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.gitignore +0 -0
  35. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/.python-version +0 -0
  36. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/AGENTS.md +0 -0
  37. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/DEVELOPMENT_PLAN.md +0 -0
  38. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/README.md +0 -0
  39. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/SESSION.md +0 -0
  40. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/SESSION_SUMMARY.md +0 -0
  41. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/claude-code-plugin-guide.md +0 -0
  42. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/installation.md +0 -0
  43. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/nightly-test.md +0 -0
  44. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-11-bulk-import-bm25-fix.md +0 -0
  45. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-11-edit-command.md +0 -0
  46. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-11-unify-format-option.md +0 -0
  47. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md +0 -0
  48. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-12-edit-content-file.md +0 -0
  49. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md +0 -0
  50. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md +0 -0
  51. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-12-fix-jfox-health-skill-kb-param.md +0 -0
  52. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-12-index-kb-param.md +0 -0
  53. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-12-lazy-import-perf.md +0 -0
  54. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-12-skill-redesign.md +0 -0
  55. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-13-add-content-file.md +0 -0
  56. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-13-bulk-import-vectorstore-init.md +0 -0
  57. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-13-ingest-log-command.md +0 -0
  58. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-13-ingest-skill-sync.md +0 -0
  59. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-13-suppress-third-party-logging.md +0 -0
  60. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-13-sync-skills-with-cli.md +0 -0
  61. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-14-sync-docs-daemon-show.md +0 -0
  62. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-15-gpu-embedding.md +0 -0
  63. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-15-make-daemon-deps-required.md +0 -0
  64. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-15-session-summary-confirmation.md +0 -0
  65. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-16-fix-windows-daemon-console-window.md +0 -0
  66. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-18-daemon-timeout-and-index-rebuild.md +0 -0
  67. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-22-release-skill.md +0 -0
  68. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-26-fix-daemon-deprecation-warnings.md +0 -0
  69. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-27-intranet-model-download.md +0 -0
  70. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-28-tag-filtering.md +0 -0
  71. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-04-29-daemon-stop-fix.md +0 -0
  72. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-03-jfox-check.md +0 -0
  73. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-03-list-notes-skip-summary.md +0 -0
  74. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-03-log-level-fix.md +0 -0
  75. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-04-list-notes-index.md +0 -0
  76. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-06-atomic-write.md +0 -0
  77. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-09-session-note-type.md +0 -0
  78. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-10-jfox-plugin.md +0 -0
  79. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-19-fix-last-used-never.md +0 -0
  80. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-05-20-modelscope-migration.md +0 -0
  81. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-06-02-todo-note-type.md +0 -0
  82. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-06-16-kimi-code-auto-summary.md +0 -0
  83. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-06-17-index-rebuild-backlinks.md +0 -0
  84. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-06-17-using-jfox-skill.md +0 -0
  85. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-06-18-self-update-command.md +0 -0
  86. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-06-21-fragment-capture.md +0 -0
  87. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-06-21-gem-synthesis.md +0 -0
  88. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-06-25-gem-synth-throttle-ledger.md +0 -0
  89. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-08-auto-summary-schedule-window.md +0 -0
  90. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-08-candidate-promote.md +0 -0
  91. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-12-gem-synth-dedup.md +0 -0
  92. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-12-release-cc-plugin-skill.md +0 -0
  93. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-12-session-summary-streamline.md +0 -0
  94. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-12-show-json.md +0 -0
  95. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-17-fix-candidate-dual-h1.md +0 -0
  96. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-18-dedup-lifecycle-decouple.md +0 -0
  97. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-19-promote-skill-three-modes.md +0 -0
  98. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-25-bookshelf.md +0 -0
  99. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-25-gem-synth-dedup-incremental-merge.md +0 -0
  100. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-26-kb-backup-restore.md +0 -0
  101. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-26-promote-skill-clear-reports-rewrite.md +0 -0
  102. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-27-release-all.md +0 -0
  103. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-28-bookshelf-flat-bundle.md +0 -0
  104. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-07-28-nightly-fulltest.md +0 -0
  105. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-08-07-session-to-permanent-cc-kimi.md +0 -0
  106. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-08-11-permanent-note-template.md +0 -0
  107. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-08-12-noteindex-frontmatter-limit-drop.md +0 -0
  108. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/plans/2026-08-15-delete-backlinks-cleanup.md +0 -0
  109. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-03-bugfixes-design.md +0 -0
  110. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md +0 -0
  111. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md +0 -0
  112. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-14-show-command-design.md +0 -0
  113. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-15-gpu-embedding-design.md +0 -0
  114. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-15-session-summary-confirmation-design.md +0 -0
  115. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-21-release-skill-design.md +0 -0
  116. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-26-fix-daemon-deprecation-warnings-design.md +0 -0
  117. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-27-intranet-model-download-design.md +0 -0
  118. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-28-tag-filtering-design.md +0 -0
  119. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-29-daemon-stop-fix-design.md +0 -0
  120. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-04-30-jfox-kb-env-var-design.md +0 -0
  121. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-05-03-jfox-check-design.md +0 -0
  122. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-05-03-list-notes-skip-summary-design.md +0 -0
  123. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-05-03-log-level-fix-design.md +0 -0
  124. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-05-04-list-notes-index-design.md +0 -0
  125. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-05-06-atomic-write-design.md +0 -0
  126. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-05-09-jfox-plugin-design.md +0 -0
  127. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-05-09-session-note-type-design.md +0 -0
  128. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-05-20-modelscope-migration-design.md +0 -0
  129. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-02-todo-note-type-design.md +0 -0
  130. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-16-kimi-code-auto-summary-design.md +0 -0
  131. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-17-index-rebuild-backlinks-design.md +0 -0
  132. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-17-using-jfox-skill-design.md +0 -0
  133. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-18-archive-soft-delete-design.md +0 -0
  134. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-18-self-update-command-design.md +0 -0
  135. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-21-fragment-capture-design.md +0 -0
  136. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-21-gem-synthesis-design.md +0 -0
  137. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-06-25-gem-synth-throttle-ledger-design.md +0 -0
  138. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-08-candidate-promote-design.md +0 -0
  139. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-12-gem-synth-dedup-design.md +0 -0
  140. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-12-release-cc-plugin-skill-design.md +0 -0
  141. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-12-session-summary-streamline-design.md +0 -0
  142. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-12-show-json-design.md +0 -0
  143. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-18-dedup-lifecycle-decouple-design.md +0 -0
  144. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-19-promote-skill-three-modes-design.md +0 -0
  145. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-25-bookshelf-design.md +0 -0
  146. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-25-gem-synth-dedup-incremental-merge-design.md +0 -0
  147. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-26-kb-backup-restore-design.md +0 -0
  148. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-26-promote-skill-clear-reports-rewrite-design.md +0 -0
  149. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-27-release-all-orchestration-design.md +0 -0
  150. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-28-bookshelf-flat-bundle-design.md +0 -0
  151. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-07-28-nightly-fulltest-design.md +0 -0
  152. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-08-07-session-to-permanent-cc-kimi-design.md +0 -0
  153. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-08-11-permanent-note-template-design.md +0 -0
  154. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-08-12-noteindex-frontmatter-limit-drop-design.md +0 -0
  155. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/superpowers/specs/2026-08-15-delete-backlinks-cleanup-design.md +0 -0
  156. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/docs/troubleshooting.md +0 -0
  157. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jessica-jones-static-cable.md +0 -0
  158. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/__main__.py +0 -0
  159. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/__init__.py +0 -0
  160. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/cli.py +0 -0
  161. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/extractor.py +0 -0
  162. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/kimi_source.py +0 -0
  163. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/ledger.py +0 -0
  164. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/loop.py +0 -0
  165. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/runner.py +0 -0
  166. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/scanner.py +0 -0
  167. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/schedule.py +0 -0
  168. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/auto_summary/sources.py +0 -0
  169. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/backup/__init__.py +0 -0
  170. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/backup/cli.py +0 -0
  171. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/backup/loop.py +0 -0
  172. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/backup/manager.py +0 -0
  173. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/backup/schedule.py +0 -0
  174. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/bookshelf/__init__.py +0 -0
  175. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/bookshelf/cli.py +0 -0
  176. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/bookshelf/meta.py +0 -0
  177. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/bookshelf/store.py +0 -0
  178. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/config.py +0 -0
  179. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/daemon/__init__.py +0 -0
  180. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/daemon/__main__.py +0 -0
  181. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/daemon/client.py +0 -0
  182. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/daemon/process.py +0 -0
  183. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/daemon/server.py +0 -0
  184. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/embedding_backend.py +0 -0
  185. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/formatters.py +0 -0
  186. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/fragment/__init__.py +0 -0
  187. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/fragment/cli.py +0 -0
  188. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/fragment/detector.py +0 -0
  189. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/fragment/internal_sources.py +0 -0
  190. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/fragment/service.py +0 -0
  191. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/fragment/store.py +0 -0
  192. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/__init__.py +0 -0
  193. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/anchors.py +0 -0
  194. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/cli.py +0 -0
  195. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/dedup.py +0 -0
  196. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/grounding.py +0 -0
  197. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/lifecycle.py +0 -0
  198. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/llm.py +0 -0
  199. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/loop.py +0 -0
  200. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/paths.py +0 -0
  201. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/store.py +0 -0
  202. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/synthesizer.py +0 -0
  203. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/gem_synth/transcript.py +0 -0
  204. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/git_extractor.py +0 -0
  205. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/global_config.py +0 -0
  206. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/graph.py +0 -0
  207. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/indexer.py +0 -0
  208. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/kb_manager.py +0 -0
  209. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/model_downloader.py +0 -0
  210. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/models.py +0 -0
  211. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/note.py +0 -0
  212. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/note_index.py +0 -0
  213. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/performance.py +0 -0
  214. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/template.py +0 -0
  215. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/template_cli.py +0 -0
  216. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/utils.py +0 -0
  217. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/jfox/vector_store.py +0 -0
  218. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/package.json +0 -0
  219. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/.claude-plugin/plugin.json +0 -0
  220. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/hooks/fragment-capture.sh +0 -0
  221. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/hooks/hooks.json +0 -0
  222. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/bookshelf/SKILL.md +0 -0
  223. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/ingest/SKILL.md +0 -0
  224. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/manage/SKILL.md +0 -0
  225. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/organize/SKILL.md +0 -0
  226. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/promote/SKILL.md +0 -0
  227. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/search/SKILL.md +0 -0
  228. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/session-summary/SKILL.md +0 -0
  229. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/session-to-permanent/SKILL.md +0 -0
  230. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/cc-plugin/skills/using-jfox/SKILL.md +0 -0
  231. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/README.md +0 -0
  232. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/kimi.plugin.json +0 -0
  233. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-admin/SKILL.md +0 -0
  234. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-auto-summary/SKILL.md +0 -0
  235. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-ingest/SKILL.md +0 -0
  236. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-manage/SKILL.md +0 -0
  237. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-organize/SKILL.md +0 -0
  238. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-promote/SKILL.md +0 -0
  239. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-search/SKILL.md +0 -0
  240. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-session-summary/SKILL.md +0 -0
  241. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-session-to-permanent/SKILL.md +0 -0
  242. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/jfox-template/SKILL.md +0 -0
  243. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/packages/kimi-plugin/skills/using-jfox/SKILL.md +0 -0
  244. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/pytest.ini +0 -0
  245. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/run_full_test.ps1 +0 -0
  246. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/scripts/download-model-intranet.sh +0 -0
  247. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/scripts/nightly_test.sh +0 -0
  248. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/scripts/nightly_test_helpers.py +0 -0
  249. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/README.md +0 -0
  250. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/kimi-cli/jfox-common/SKILL.md +0 -0
  251. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/kimi-cli/jfox-ingest/SKILL.md +0 -0
  252. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/kimi-cli/jfox-organize/SKILL.md +0 -0
  253. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/kimi-cli/jfox-search/SKILL.md +0 -0
  254. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/kimi-cli/jfox-session-summary/SKILL.md +0 -0
  255. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-bookshelf/SKILL.md +0 -0
  256. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-ci/SKILL.md +0 -0
  257. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-common/SKILL.md +0 -0
  258. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-ingest/SKILL.md +0 -0
  259. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-organize/SKILL.md +0 -0
  260. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-overview/SKILL.md +0 -0
  261. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-promote/SKILL.md +0 -0
  262. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-release/SKILL.md +0 -0
  263. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-release-all/SKILL.md +0 -0
  264. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-release-cc-plugin/SKILL.md +0 -0
  265. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-release-kimi-plugin/SKILL.md +0 -0
  266. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-search/SKILL.md +0 -0
  267. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-session-summary/SKILL.md +0 -0
  268. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/skills-recommend/pi/jfox-session-to-permanent/SKILL.md +0 -0
  269. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/COVERAGE_PLAN.md +0 -0
  270. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/MIGRATION.md +0 -0
  271. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/TESTS.md +0 -0
  272. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/conftest.py +0 -0
  273. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/__init__.py +0 -0
  274. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/test_auto_summary_kimi.py +0 -0
  275. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/test_backlinks.py +0 -0
  276. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/test_candidate_promote_flow.py +0 -0
  277. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/test_fragment_capture_flow.py +0 -0
  278. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/test_gem_synth_flow.py +0 -0
  279. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/test_index_rebuild_backlinks.py +0 -0
  280. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/test_model_download.py +0 -0
  281. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/integration/test_tag_filter_cli.py +0 -0
  282. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/performance/__init__.py +0 -0
  283. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/performance/test_performance.py +0 -0
  284. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_advanced_features.py +0 -0
  285. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_cli_format.py +0 -0
  286. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_config_set_unit.py +0 -0
  287. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_config_unit.py +0 -0
  288. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_core_workflow.py +0 -0
  289. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_embedding_device.py +0 -0
  290. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_hybrid_search.py +0 -0
  291. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_integration.py +0 -0
  292. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_kb_current.py +0 -0
  293. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/test_suggest_links.py +0 -0
  294. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/__init__.py +0 -0
  295. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/bookshelf/__init__.py +0 -0
  296. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/bookshelf/conftest.py +0 -0
  297. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/bookshelf/test_cli.py +0 -0
  298. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/bookshelf/test_meta.py +0 -0
  299. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/bookshelf/test_store.py +0 -0
  300. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/conftest.py +0 -0
  301. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_add_backfill_links.py +0 -0
  302. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_archive.py +0 -0
  303. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_atomic_write.py +0 -0
  304. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_cli.py +0 -0
  305. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_config_sources.py +0 -0
  306. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_defaults.py +0 -0
  307. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_extractor.py +0 -0
  308. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_ledger.py +0 -0
  309. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_loop.py +0 -0
  310. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_runner.py +0 -0
  311. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_scanner.py +0 -0
  312. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_schedule.py +0 -0
  313. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_auto_summary_status.py +0 -0
  314. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_backup.py +0 -0
  315. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_bm25_batch.py +0 -0
  316. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_check.py +0 -0
  317. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_content_file.py +0 -0
  318. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_daemon_cli.py +0 -0
  319. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_daemon_process.py +0 -0
  320. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_delete_backlink_cleanup.py +0 -0
  321. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_edit.py +0 -0
  322. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_embedding_local_load.py +0 -0
  323. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_format_unify.py +0 -0
  324. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_formatters.py +0 -0
  325. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_fragment_cli.py +0 -0
  326. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_fragment_config.py +0 -0
  327. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_fragment_detector.py +0 -0
  328. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_fragment_hook.py +0 -0
  329. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_fragment_internal_sources.py +0 -0
  330. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_fragment_service.py +0 -0
  331. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_fragment_store.py +0 -0
  332. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_anchors.py +0 -0
  333. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_backfill.py +0 -0
  334. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_cli.py +0 -0
  335. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_config.py +0 -0
  336. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_config_dedup.py +0 -0
  337. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_dedup.py +0 -0
  338. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_grounding.py +0 -0
  339. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_lifecycle.py +0 -0
  340. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_llm.py +0 -0
  341. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_loop.py +0 -0
  342. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_store.py +0 -0
  343. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_store_duplicate.py +0 -0
  344. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_synthesizer.py +0 -0
  345. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_gem_synth_transcript.py +0 -0
  346. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_git_extractor.py +0 -0
  347. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_global_config.py +0 -0
  348. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_index_kb_param.py +0 -0
  349. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_indexer_clear_before_rebuild.py +0 -0
  350. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_indexer_verify.py +0 -0
  351. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_kb_manager.py +0 -0
  352. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_kimi_extractor.py +0 -0
  353. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_kimi_plugin_skills.py +0 -0
  354. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_kimi_scanner.py +0 -0
  355. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_lazy_import.py +0 -0
  356. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_ledger_migration.py +0 -0
  357. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_list_notes_skip.py +0 -0
  358. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_logging_config.py +0 -0
  359. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_model_downloader.py +0 -0
  360. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_nightly_test_helpers.py +0 -0
  361. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_note_candidate_fields.py +0 -0
  362. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_note_candidate_filename.py +0 -0
  363. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_note_dedup_sync.py +0 -0
  364. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_note_index.py +0 -0
  365. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_note_lifecycle_hooks.py +0 -0
  366. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_note_promote.py +0 -0
  367. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_note_type_candidate.py +0 -0
  368. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_rebuild_backlinks_impl.py +0 -0
  369. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_release_all_helper.py +0 -0
  370. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_release_cc_plugin_helper.py +0 -0
  371. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_release_helper.py +0 -0
  372. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_release_helper_verify.py +0 -0
  373. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_release_kimi_plugin_helper.py +0 -0
  374. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_runner_multi_source.py +0 -0
  375. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_scanner_list.py +0 -0
  376. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_session_file_source.py +0 -0
  377. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_session_note.py +0 -0
  378. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_session_source.py +0 -0
  379. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_show.py +0 -0
  380. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_synthesizer_dedup.py +0 -0
  381. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_system_prompt_sections.py +0 -0
  382. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_tag_filter.py +0 -0
  383. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_template.py +0 -0
  384. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_template_cli.py +0 -0
  385. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_use_kb_env_var.py +0 -0
  386. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_using_jfox_skill.py +0 -0
  387. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/unit/test_vector_store_clear.py +0 -0
  388. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/utils/__init__.py +0 -0
  389. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/utils/assertions.py +0 -0
  390. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/utils/jfox_cli.py +0 -0
  391. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/utils/note_generator.py +0 -0
  392. {jfox_cli-1.7.1 → jfox_cli-1.7.2}/tests/utils/temp_kb.py +0 -0
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to jfox-cli will be documented in this file.
4
4
 
5
+ ## [1.7.2] - 2026-08-21
6
+
7
+ ### Fixes
8
+ - **bm25**: 孤儿 tmp 无效时覆盖分支版本撞号,clear 数据复活(#403) (#404)
9
+ - **bm25**: PR #396 cc round-4 遗留修复——clear 快照化/单边态自愈/tmp 消费闭环(#401) (#402)
10
+ - **bm25**: 索引并发写乐观并发控制,根治 daemon 旧快照覆盖回滚(#391) (#396)
11
+ - **update**: 升级期间显示进度提示,stdin 指向 DEVNULL 防挂住 (#394) (#395)
12
+
13
+ ### Changes
14
+ - style(bm25): collapse multi-line condition to single line (pi-lens format)
15
+ - **claude.md**: 记 delete_note backlinks 增量清理语义 (#388) (#397)
16
+ - **claude.md**: 记 CI paths 触发限制(#382) (#384)
17
+
18
+ [1.7.2]: https://github.com/zhuxixi/jfox/compare/v1.7.1...v1.7.2
19
+
5
20
  ## [1.7.1] - 2026-08-16
6
21
 
7
22
  ### Fixes
@@ -132,6 +132,8 @@ Four jobs in `.github/workflows/integration-test.yml`:
132
132
  - **Full** (manual): All tests, all OS, all Python versions
133
133
  - **Coverage** (after fast): Runs coverage on fast tests, uploads HTML/XML artifacts
134
134
 
135
+ **CI 触发受 `paths` 限制**:`integration-test.yml` 的 `paths` 只含 `jfox/**`/`tests/**`/`pyproject.toml`/自身——`packages/`(cc/kimi-plugin 发版 bump)、`docs/`、`.claude/` 改动**不触发 CI**。后果:`packages/` 下版本 bump 不跑测试,release-helper 测试须按当前版本动态算「下一版」(勿硬编码),否则只在后续触达 `tests/` 的 PR 才暴露(#382 踩过)。
136
+
135
137
  **Release** workflow in `.github/workflows/publish.yml`: publishes to PyPI on GitHub release publication.
136
138
 
137
139
  **Local nightly full-test**(#263/#361,非 GitHub Action):`scripts/nightly_test.sh` 是本机 crontab 定时任务(crontab 行装本机、不进仓库),每周二 09:00 跑 CI `test-fast` 跳过的全量 pytest(performance/bulk/slow/embedding)——成功静默,失败按签名去重复用带 `nightly-test-failure` label 的 issue。前置:今日 #338 备份须成功(读 `~/.jfox-backup/state.json` 的 `last_ok`,否则 `SKIP: 今日备份未确认` 退出)。纯逻辑在 `scripts/nightly_test_helpers.py`(单测 `tests/unit/test_nightly_test_helpers.py`);`--dry-run` 用人造失败走 issue 流程、不跑真实 pytest。
@@ -177,3 +179,4 @@ JFox ships as a Claude Code plugin. Two-tier structure:
177
179
  - Test directory migration mostly complete; root-level `test_config_unit.py` and `test_config_set_unit.py` remain but test different things from `tests/unit/`
178
180
  - 生命周期订阅模块的重依赖(numpy 等)必须 lazy import 进回调体,不能顶层 import——`jfox/__init__.py` 每次启动都 import 订阅模块,顶层会令 `--version`/`search` 等不相关命令多付 ~70-100ms eager 加载。参考 `gem_synth/lifecycle.py`
179
181
  - `rich` Console 输出机器解析的 JSON 时须 `soft_wrap=True`:默认按 80 列硬折行,会把长字符串(如 Windows 绝对路径)在 JSON 字符串内部断行,`json.loads` 报 Invalid control character(Ubuntu 路径短不触发,只在 Windows CI 挂,#336)。参考 `bookshelf/cli.py` `_emit_json`
182
+ - `delete_note`/`promote_note` 增量同步各 target 的 backlinks(#388 起对称):单 target 写盘失败仅 warning 不中断;悬空/不对称残留用 `jfox index rebuild --backlinks` 全量重算兜底
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: jfox-cli
3
- Version: 1.7.1
3
+ Version: 1.7.2
4
4
  Summary: JFox - Zettelkasten 知识管理 CLI 工具
5
5
  Project-URL: Homepage, https://github.com/zhuxixi/jfox
6
6
  Project-URL: Repository, https://github.com/zhuxixi/jfox
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Python: >=3.10
19
19
  Requires-Dist: chromadb>=0.5.0
20
20
  Requires-Dist: fastapi>=0.110.0
21
+ Requires-Dist: filelock>=3.12
21
22
  Requires-Dist: jinja2>=3.0
22
23
  Requires-Dist: networkx>=3.0
23
24
  Requires-Dist: pydantic>=2.0
@@ -0,0 +1,577 @@
1
+ # BM25 索引并发写损坏修复(#391)Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** 给 BM25Index 加乐观并发控制(write_version + 增量重放)、文件锁、原子写盘与读路径 stale 检测,根治 daemon 长进程旧内存快照覆盖磁盘导致的索引回滚。
6
+
7
+ **Architecture:** 单点改造 `jfox/bm25_index.py`:`_save()` 重写为「拿文件锁 → 比对磁盘 write_version → 必要时 reload + 重放 `_pending_ops` → 原子写 pkl(tmp+os.replace)→ 原子写 metadata(commit point)」。`jfox/search_engine.py` 构造引擎时调 `check_stale_and_reload()` 刷新长驻进程的旧单例。
8
+
9
+ **Tech Stack:** Python 3.10+, filelock(已是传递依赖,本 plan 显式声明), rank_bm25, pytest。
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-16-bm25-concurrent-write-race-design.md`
12
+
13
+ ## Global Constraints
14
+
15
+ - 行长度 100(black/ruff 配置,E501 可忽略但保持整洁)
16
+ - 失败路径一律**不写盘**:锁超时、reload 失败 → `_save()` 返回 False,磁盘保持原状
17
+ - 不引入除 filelock 外的新依赖;filelock 显式声明进 `pyproject.toml`
18
+ - 所有新增/修改的公开行为必须有测试;测试放 `tests/unit/test_bm25_concurrency.py`
19
+ - 中文注释,conventional commits,按文件 stage(不用 `git add -A`)
20
+ - 不改动 `add_document`/`remove_document`/`rebuild_from_notes`/`search` 的对外签名与语义
21
+
22
+ ---
23
+
24
+ ### Task 1: `_save` 重写——文件锁 + 原子写 + write_version + 日志
25
+
26
+ **Files:**
27
+ - Modify: `jfox/bm25_index.py`(`_save`、`_load`、`__init__`、新增 `_read_disk_write_version` / `_atomic_write_bytes` / `_atomic_write_text`)
28
+ - Modify: `pyproject.toml`(dependencies 加 `filelock>=3.12`)
29
+ - Test: `tests/unit/test_bm25_concurrency.py`(新建)
30
+
31
+ **Interfaces:**
32
+ - Produces:
33
+ - `BM25Index._loaded_write_version: int`(实例属性,load 后 = 磁盘版本)
34
+ - `BM25Index._read_disk_write_version() -> int`
35
+ - metadata.json 新字段 `write_version: int`
36
+
37
+ - [ ] **Step 1: 写失败测试**
38
+
39
+ 新建 `tests/unit/test_bm25_concurrency.py`:
40
+
41
+ ```python
42
+ """
43
+ BM25Index 并发写防护(#391)单元测试
44
+
45
+ 用两个共享同一索引目录的 BM25Index 实例模拟 CLI 与 daemon 两个进程的交错写。
46
+ """
47
+
48
+ import json
49
+ import pickle
50
+ from pathlib import Path
51
+ from unittest.mock import patch
52
+
53
+ import pytest
54
+ from filelock import Timeout
55
+
56
+ from jfox.bm25_index import BM25Index
57
+
58
+
59
+ def _load_disk_ids(index_dir: Path) -> list:
60
+ with open(index_dir / BM25Index.INDEX_FILENAME, "rb") as f:
61
+ return pickle.load(f)["doc_ids"]
62
+
63
+
64
+ def _disk_version(index_dir: Path) -> int:
65
+ with open(index_dir / BM25Index.METADATA_FILENAME, "r", encoding="utf-8") as f:
66
+ return json.load(f).get("write_version", 0)
67
+
68
+
69
+ class TestWriteVersionAndLock:
70
+ """write_version 元数据、原子写、文件锁"""
71
+
72
+ def test_write_version_increments(self, tmp_path):
73
+ idx = BM25Index(index_dir=tmp_path)
74
+ assert idx.add_document("a", "hello world", "session")
75
+ assert idx.add_document("b", "foo bar", "session")
76
+ assert _disk_version(tmp_path) == 2
77
+ meta = json.loads(
78
+ (tmp_path / BM25Index.METADATA_FILENAME).read_text(encoding="utf-8")
79
+ )
80
+ assert meta["doc_count"] == 2
81
+ assert meta["write_version"] == 2
82
+
83
+ def test_legacy_metadata_without_version(self, tmp_path):
84
+ idx = BM25Index(index_dir=tmp_path)
85
+ assert idx.add_document("a", "hello world", "session")
86
+ # 抹掉 write_version 模拟旧格式文件
87
+ meta_path = tmp_path / BM25Index.METADATA_FILENAME
88
+ meta = json.loads(meta_path.read_text(encoding="utf-8"))
89
+ meta.pop("write_version", None)
90
+ meta_path.write_text(json.dumps(meta), encoding="utf-8")
91
+ idx2 = BM25Index(index_dir=tmp_path)
92
+ assert idx2._loaded_write_version == 0
93
+ assert "a" in idx2.doc_mapping
94
+ assert idx2.add_document("b", "foo bar", "session")
95
+ assert _disk_version(tmp_path) >= 1
96
+
97
+ def test_lock_timeout_aborts_save_without_writing(self, tmp_path):
98
+ idx = BM25Index(index_dir=tmp_path)
99
+ with patch("jfox.bm25_index.FileLock") as mock_lock_cls:
100
+ mock_lock_cls.return_value.__enter__.side_effect = Timeout("bm25_index.lock")
101
+ ok = idx._save()
102
+ assert ok is False
103
+ assert not (tmp_path / BM25Index.INDEX_FILENAME).exists()
104
+ ```
105
+
106
+ - [ ] **Step 2: 跑测试确认失败**
107
+
108
+ Run: `cd <worktree> && uv run pytest tests/unit/test_bm25_concurrency.py -v`
109
+ Expected: FAIL(`_loaded_write_version` 属性不存在 / FileLock 未导入)
110
+
111
+ - [ ] **Step 3: 实现**
112
+
113
+ `jfox/bm25_index.py` 顶部 import 区加:
114
+
115
+ ```python
116
+ import os
117
+
118
+ from filelock import FileLock, Timeout
119
+ ```
120
+
121
+ `BM25Index` 类常量区加 `LOCK_FILENAME = "bm25_index.lock"`;`__init__` 中 `self._load()` 之前初始化:
122
+
123
+ ```python
124
+ self._loaded_write_version: int = 0 # load 时记录的磁盘写入版本(乐观锁令牌)
125
+ self._pending_ops: List[Tuple[str, str, str, Optional[str]]] = [] # 未落盘的增量操作
126
+ self._dirty_full_rebuild: bool = False # rebuild 后 save 走覆盖语义
127
+ ```
128
+
129
+ `_load()` 在读完 metadata 后记录版本(`metadata = json.load(f)` 之后):
130
+
131
+ ```python
132
+ self._loaded_write_version = int(metadata.get("write_version") or 0)
133
+ ```
134
+
135
+ 新增工具方法与 `_save` 重写(整体替换现有 `_save`):
136
+
137
+ ```python
138
+ def _read_disk_write_version(self) -> int:
139
+ """读磁盘 metadata 的 write_version;损坏/缺失视为 0"""
140
+ try:
141
+ with open(self.metadata_path, "r", encoding="utf-8") as f:
142
+ return int(json.load(f).get("write_version") or 0)
143
+ except Exception:
144
+ return 0
145
+
146
+ def _atomic_write_bytes(self, path: Path, data: bytes) -> None:
147
+ """原子写:先写临时文件再 os.replace,读端永远只能读到完整文件"""
148
+ tmp = path.with_suffix(path.suffix + ".tmp")
149
+ with open(tmp, "wb") as f:
150
+ f.write(data)
151
+ os.replace(tmp, path)
152
+
153
+ def _atomic_write_text(self, path: Path, text: str) -> None:
154
+ tmp = path.with_suffix(path.suffix + ".tmp")
155
+ with open(tmp, "w", encoding="utf-8") as f:
156
+ f.write(text)
157
+ os.replace(tmp, path)
158
+
159
+ def _save(self) -> bool:
160
+ """
161
+ 保存索引到磁盘(乐观并发控制版)
162
+
163
+ 流程:拿文件锁 → 比对磁盘 write_version → 磁盘较新则 reload+重放本地增量
164
+ → 原子写 pkl → 原子写 metadata(commit point)。
165
+ 铁律:任何一步失败都不写盘,返回 False。
166
+ """
167
+ try:
168
+ with FileLock(str(self.index_dir / self.LOCK_FILENAME), timeout=5):
169
+ disk_version = self._read_disk_write_version()
170
+ if disk_version > self._loaded_write_version and not self._dirty_full_rebuild:
171
+ if not self._load():
172
+ logger.error(
173
+ "BM25 磁盘版本较新但 reload 失败,放弃本次 save(不写盘)"
174
+ )
175
+ return False
176
+ self._replay_pending_ops()
177
+ elif disk_version < self._loaded_write_version:
178
+ logger.warning(
179
+ f"BM25 磁盘版本 {disk_version} 比本地 {self._loaded_write_version} 旧,按本地覆盖"
180
+ )
181
+
182
+ new_version = max(disk_version, self._loaded_write_version) + 1
183
+ prev_version = self._loaded_write_version
184
+
185
+ # 先写 pkl(数据本体),后写 metadata(commit point)
186
+ index_data = {
187
+ "bm25": self.bm25,
188
+ "documents": self.documents,
189
+ "doc_ids": self.doc_ids,
190
+ "doc_types": self.doc_types,
191
+ "doc_mapping": self.doc_mapping,
192
+ }
193
+ self._atomic_write_bytes(self.index_path, pickle.dumps(index_data))
194
+ metadata = {
195
+ "version": self.INDEX_VERSION,
196
+ "doc_count": len(self.doc_ids),
197
+ "needs_rebuild": self.needs_rebuild,
198
+ "write_version": new_version,
199
+ }
200
+ self._atomic_write_text(
201
+ self.metadata_path, json.dumps(metadata, ensure_ascii=False, indent=2)
202
+ )
203
+
204
+ self._loaded_write_version = new_version
205
+ self._pending_ops.clear()
206
+ self._dirty_full_rebuild = False
207
+ logger.info(
208
+ f"Saved BM25 index: {len(self.doc_ids)} documents "
209
+ f"(write_version={new_version}, prev={prev_version})"
210
+ )
211
+ return True
212
+ except Timeout:
213
+ logger.error("BM25 save: 获取索引文件锁超时(5s),放弃写入")
214
+ return False
215
+ except Exception as e:
216
+ logger.error(f"Failed to save BM25 index: {e}")
217
+ return False
218
+ ```
219
+
220
+ `_replay_pending_ops` 在 Task 2 才实现,本 Task 先放桩(保证 self._pending_ops 恒为空时行为正确):
221
+
222
+ ```python
223
+ def _replay_pending_ops(self) -> None:
224
+ """重放本地未落盘的增量操作(Task 2 实现合并逻辑)"""
225
+ ```
226
+
227
+ `pyproject.toml` dependencies 加 `"filelock>=3.12"`,然后 `uv lock`。
228
+
229
+ - [ ] **Step 4: 跑测试确认通过**
230
+
231
+ Run: `uv run pytest tests/unit/test_bm25_concurrency.py -v`
232
+ Expected: 3 个测试 PASS
233
+
234
+ - [ ] **Step 5: 回归现有 BM25 测试**
235
+
236
+ Run: `uv run pytest tests/unit/test_bm25_batch.py tests/unit/test_indexer_verify.py -v`
237
+ Expected: 全绿(`_save` 签名/行为对外不变)
238
+
239
+ - [ ] **Step 6: Commit**
240
+
241
+ ```bash
242
+ git add jfox/bm25_index.py pyproject.toml uv.lock tests/unit/test_bm25_concurrency.py
243
+ git commit -m "feat(bm25): 索引写入加文件锁+原子写+write_version 乐观锁令牌(#391)"
244
+ ```
245
+
246
+ ---
247
+
248
+ ### Task 2: 增量操作跟踪与重放合并(乐观锁核心)
249
+
250
+ **Files:**
251
+ - Modify: `jfox/bm25_index.py`(`add_document`/`remove_document`/`add_documents_batch` 拆出纯内存 helper,记录 `_pending_ops`;实现 `_replay_pending_ops`)
252
+ - Test: `tests/unit/test_bm25_concurrency.py`
253
+
254
+ **Interfaces:**
255
+ - Consumes: Task 1 的 `_save` 新流程、`_pending_ops`、`_loaded_write_version`
256
+ - Produces:
257
+ - `BM25Index._add_document_local(note_id, content, note_type) -> bool`(纯内存,不 save 不记 pending)
258
+ - `BM25Index._remove_document_local(note_id) -> bool`(纯内存)
259
+ - `BM25Index._replay_pending_ops() -> None`(同 id 合并后按序重放)
260
+
261
+ - [ ] **Step 1: 写失败测试(追加到同文件)**
262
+
263
+ ```python
264
+ class TestOptimisticMerge:
265
+ """双实例模拟双进程:磁盘较新时 reload + 重放本地增量"""
266
+
267
+ def test_concurrent_add_replay(self, tmp_path):
268
+ a = BM25Index(index_dir=tmp_path) # 模拟 daemon:先 load 旧状态
269
+ b = BM25Index(index_dir=tmp_path) # 模拟 CLI
270
+ assert b.add_document("note-cli", "cli 写入 hello", "session") # v1
271
+ assert a.add_document("note-daemon", "daemon 写入 world", "session") # 触发 merge → v2
272
+ ids = _load_disk_ids(tmp_path)
273
+ assert "note-cli" in ids
274
+ assert "note-daemon" in ids
275
+ assert _disk_version(tmp_path) == 2
276
+
277
+ def test_concurrent_remove_replay(self, tmp_path):
278
+ a = BM25Index(index_dir=tmp_path)
279
+ assert a.add_document("x", "hello x", "permanent") # v1
280
+ b = BM25Index(index_dir=tmp_path) # b load v1(含 x)
281
+ assert b.add_document("y", "hello y", "permanent") # v2
282
+ assert a.remove_document("x") # a:reload v2 + 重放 remove x → v3
283
+ ids = _load_disk_ids(tmp_path)
284
+ assert "y" in ids
285
+ assert "x" not in ids
286
+ assert _disk_version(tmp_path) == 3
287
+
288
+ def test_conflict_last_writer_wins(self, tmp_path):
289
+ a = BM25Index(index_dir=tmp_path)
290
+ assert a.add_document("x", "from a", "session") # v1
291
+ b = BM25Index(index_dir=tmp_path)
292
+ assert b.add_document("x", "from b", "session") # v2
293
+ assert a.remove_document("x") # v3:重放 remove 覆盖 b 的 add
294
+ assert "x" not in _load_disk_ids(tmp_path)
295
+
296
+ def test_reload_failure_aborts_save_without_writing(self, tmp_path):
297
+ a = BM25Index(index_dir=tmp_path)
298
+ b = BM25Index(index_dir=tmp_path)
299
+ assert b.add_document("x", "hello x", "session") # v1
300
+ # a 内存里产生未落盘的增量,然后破坏磁盘 pkl 使 reload 失败
301
+ a._add_document_local("y", "local change", "session")
302
+ a._pending_ops.append(("add", "y", "local change", "session"))
303
+ corrupted = b"corrupted-pkl"
304
+ (tmp_path / BM25Index.INDEX_FILENAME).write_bytes(corrupted)
305
+ assert a._save() is False
306
+ assert (tmp_path / BM25Index.INDEX_FILENAME).read_bytes() == corrupted
307
+ ```
308
+
309
+ - [ ] **Step 2: 跑测试确认失败**
310
+
311
+ Run: `uv run pytest tests/unit/test_bm25_concurrency.py::TestOptimisticMerge -v`
312
+ Expected: FAIL(`_add_document_local` 不存在 / merge 不生效)
313
+
314
+ - [ ] **Step 3: 实现**
315
+
316
+ 拆出纯内存 helper(不含 `_rebuild_index`/`_save`/pending 记录):
317
+
318
+ ```python
319
+ def _add_document_local(self, note_id: str, content: str, note_type: Optional[str]) -> bool:
320
+ """纯内存添加(不含索引重建/落盘/pending 记录)"""
321
+ if note_id in self.doc_mapping:
322
+ self._remove_document_local(note_id)
323
+ tokens = self._tokenize(content)
324
+ if not tokens:
325
+ return True
326
+ normalized_type = self._normalize_note_type(note_type)
327
+ idx = len(self.documents)
328
+ self.documents.append(tokens)
329
+ self.doc_ids.append(note_id)
330
+ self.doc_types.append(normalized_type)
331
+ self.doc_mapping[note_id] = idx
332
+ return True
333
+
334
+ def _remove_document_local(self, note_id: str) -> bool:
335
+ """纯内存移除"""
336
+ if note_id not in self.doc_mapping:
337
+ return True
338
+ idx = self.doc_mapping[note_id]
339
+ self.documents.pop(idx)
340
+ self.doc_ids.pop(idx)
341
+ self.doc_types.pop(idx)
342
+ del self.doc_mapping[note_id]
343
+ self.doc_mapping = {doc_id: i for i, doc_id in enumerate(self.doc_ids)}
344
+ return True
345
+ ```
346
+
347
+ `add_document` 主体改为(语义不变:空 tokens 静默成功;已存在先移除):
348
+
349
+ ```python
350
+ try:
351
+ if not self._add_document_local(note_id, content, note_type):
352
+ return False
353
+ self._pending_ops.append(("add", note_id, content, note_type))
354
+ self._rebuild_index()
355
+ self._save()
356
+ return True
357
+ except Exception as e:
358
+ logger.error(f"Failed to add document {note_id}: {e}")
359
+ return False
360
+ ```
361
+
362
+ `remove_document` 主体改为(不存在则直接返回 True、不记 pending 不 save,保持原语义):
363
+
364
+ ```python
365
+ try:
366
+ if note_id not in self.doc_mapping:
367
+ return True
368
+ self._remove_document_local(note_id)
369
+ self._pending_ops.append(("remove", note_id, "", None))
370
+ self._rebuild_index()
371
+ self._save()
372
+ return True
373
+ except Exception as e:
374
+ logger.error(f"Failed to remove document {note_id}: {e}")
375
+ return False
376
+ ```
377
+
378
+ 实现 `_replay_pending_ops`(替换 Task 1 的桩):
379
+
380
+ ```python
381
+ def _replay_pending_ops(self) -> None:
382
+ """重放本地未落盘的增量操作:同 id 合并(只留最后 op)后按序 apply"""
383
+ if not self._pending_ops:
384
+ return
385
+ merged: Dict[str, Tuple[str, str, Optional[str]]] = {}
386
+ for op, nid, content, ntype in self._pending_ops:
387
+ merged[nid] = (op, content, ntype)
388
+ logger.warning(
389
+ f"BM25 merge: 磁盘版本较新,重放 {len(merged)} 条本地操作后合并写入"
390
+ )
391
+ for nid, (op, content, ntype) in merged.items():
392
+ if op == "remove":
393
+ self._remove_document_local(nid)
394
+ else:
395
+ self._add_document_local(nid, content, ntype)
396
+ self._rebuild_index()
397
+ ```
398
+
399
+ `add_documents_batch` 内部若直接操作列表:改为逐条 `_add_document_local` 并把每条 append 进 `_pending_ops`(batch 场景 pending 条数=batch 大小,可接受;重放时按 id 合并去重)。
400
+
401
+ - [ ] **Step 4: 跑测试确认通过**
402
+
403
+ Run: `uv run pytest tests/unit/test_bm25_concurrency.py -v`
404
+ Expected: 全部 PASS
405
+
406
+ - [ ] **Step 5: Commit**
407
+
408
+ ```bash
409
+ git add jfox/bm25_index.py tests/unit/test_bm25_concurrency.py
410
+ git commit -m "feat(bm25): 写前乐观检测+增量重放合并,根治旧快照覆盖(#391)"
411
+ ```
412
+
413
+ ---
414
+
415
+ ### Task 3: rebuild 覆盖语义
416
+
417
+ **Files:**
418
+ - Modify: `jfox/bm25_index.py`(`rebuild_from_notes` 置 `_dirty_full_rebuild`)
419
+ - Test: `tests/unit/test_bm25_concurrency.py`
420
+
421
+ **Interfaces:**
422
+ - Consumes: Task 1/2 的 `_save`、`_dirty_full_rebuild`
423
+
424
+ - [ ] **Step 1: 写失败测试**
425
+
426
+ ```python
427
+ from datetime import datetime
428
+
429
+ from jfox.models import Note, NoteType
430
+
431
+
432
+ def _note(nid: str, ntype: NoteType = NoteType.PERMANENT) -> Note:
433
+ return Note(
434
+ id=nid,
435
+ title=f"title {nid}",
436
+ type=ntype,
437
+ content=f"content {nid}",
438
+ created=datetime.now(),
439
+ updated=datetime.now(),
440
+ )
441
+
442
+
443
+ class TestRebuildSemantics:
444
+ def test_rebuild_overwrites_stale_disk(self, tmp_path):
445
+ a = BM25Index(index_dir=tmp_path)
446
+ b = BM25Index(index_dir=tmp_path)
447
+ assert b.add_document("old", "old note", "session") # v1
448
+ # a 内存是旧状态,但 rebuild 语义=以我的快照为准:直接覆盖,不 merge
449
+ assert a.rebuild_from_notes([_note("n1"), _note("n2")])
450
+ assert set(_load_disk_ids(tmp_path)) == {"n1", "n2"}
451
+ ```
452
+
453
+ - [ ] **Step 2: 跑测试确认失败**
454
+
455
+ Run: `uv run pytest tests/unit/test_bm25_concurrency.py::TestRebuildSemantics -v`
456
+ Expected: FAIL(当前 rebuild 后 `_dirty_full_rebuild` 未置位,走 merge 分支会保留 "old")
457
+
458
+ - [ ] **Step 3: 实现**
459
+
460
+ `rebuild_from_notes` 原子替换当前状态成功后、调 `_save()` 之前:
461
+
462
+ ```python
463
+ # rebuild 语义=以本次快照为准:save 时即便磁盘较新也直接覆盖,不做 merge
464
+ self._dirty_full_rebuild = True
465
+ self._pending_ops.clear()
466
+ ```
467
+
468
+ - [ ] **Step 4: 跑测试确认通过**
469
+
470
+ Run: `uv run pytest tests/unit/test_bm25_concurrency.py -v`
471
+ Expected: 全部 PASS
472
+
473
+ - [ ] **Step 5: Commit**
474
+
475
+ ```bash
476
+ git add jfox/bm25_index.py tests/unit/test_bm25_concurrency.py
477
+ git commit -m "feat(bm25): rebuild 采用覆盖语义(stale 时不 merge)(#391)"
478
+ ```
479
+
480
+ ---
481
+
482
+ ### Task 4: 读路径 stale 检测(daemon 搜索不滞后)
483
+
484
+ **Files:**
485
+ - Modify: `jfox/bm25_index.py`(新增 `check_stale_and_reload`)
486
+ - Modify: `jfox/search_engine.py`(`HybridSearchEngine.__init__` 接线)
487
+ - Test: `tests/unit/test_bm25_concurrency.py`
488
+
489
+ **Interfaces:**
490
+ - Produces: `BM25Index.check_stale_and_reload() -> None`(磁盘版本较新则 reload,失败静默兜底)
491
+
492
+ - [ ] **Step 1: 写失败测试**
493
+
494
+ ```python
495
+ class TestStaleDetection:
496
+ def test_check_stale_and_reload(self, tmp_path):
497
+ a = BM25Index(index_dir=tmp_path)
498
+ b = BM25Index(index_dir=tmp_path)
499
+ assert b.add_document("x", "hello x", "session") # v1
500
+ assert "x" not in a.doc_mapping # a 仍是旧内存
501
+ a.check_stale_and_reload()
502
+ assert "x" in a.doc_mapping
503
+
504
+ def test_check_stale_and_reload_noop_when_fresh(self, tmp_path):
505
+ a = BM25Index(index_dir=tmp_path)
506
+ assert a.add_document("x", "hello x", "session")
507
+ docs_before = list(a.documents)
508
+ a.check_stale_and_reload()
509
+ assert a.documents == docs_before # 未发生 reload
510
+ ```
511
+
512
+ - [ ] **Step 2: 跑测试确认失败**
513
+
514
+ Run: `uv run pytest tests/unit/test_bm25_concurrency.py::TestStaleDetection -v`
515
+ Expected: FAIL(`check_stale_and_reload` 不存在)
516
+
517
+ - [ ] **Step 3: 实现**
518
+
519
+ `jfox/bm25_index.py` 新增:
520
+
521
+ ```python
522
+ def check_stale_and_reload(self) -> None:
523
+ """轻量 stale 检查:磁盘 write_version 比内存新就 reload。
524
+
525
+ 用于长驻进程(daemon)的查询路径,避免搜索长期基于过期快照。
526
+ 失败静默兜底(用内存快照),不阻塞查询。
527
+ """
528
+ try:
529
+ if self._read_disk_write_version() > self._loaded_write_version:
530
+ self._load()
531
+ except Exception:
532
+ pass
533
+ ```
534
+
535
+ `jfox/search_engine.py` 的 `HybridSearchEngine.__init__`,在 `self.bm25_index = bm25_index or get_bm25_index()` 之后、needs_rebuild 检查之前插入:
536
+
537
+ ```python
538
+ # 长驻进程(daemon)的查询路径:磁盘被别的进程写过时自动刷新单例,
539
+ # 避免 hybrid 搜索长期基于过期快照(#391)
540
+ self.bm25_index.check_stale_and_reload()
541
+ ```
542
+
543
+ - [ ] **Step 4: 跑测试确认通过**
544
+
545
+ Run: `uv run pytest tests/unit/test_bm25_concurrency.py -v`
546
+ Expected: 全部 PASS
547
+
548
+ - [ ] **Step 5: Commit**
549
+
550
+ ```bash
551
+ git add jfox/bm25_index.py jfox/search_engine.py tests/unit/test_bm25_concurrency.py
552
+ git commit -m "feat(search): 引擎构造时检测 BM25 磁盘版本并自动刷新(#391)"
553
+ ```
554
+
555
+ ---
556
+
557
+ ### Task 5: 回归与收尾
558
+
559
+ **Files:**
560
+ - Modify: 无新代码
561
+
562
+ - [ ] **Step 1: 全量快速测试回归**
563
+
564
+ Run: `uv run pytest tests/unit/ -m "not embedding and not slow" -x -q`
565
+ Expected: 全绿。重点关注:`test_bm25_batch.py`、`test_indexer_verify.py`、search 相关测试。
566
+
567
+ - [ ] **Step 2: ruff + black**
568
+
569
+ Run: `uv run ruff check jfox/bm25_index.py jfox/search_engine.py tests/unit/test_bm25_concurrency.py && uv run black --check jfox/bm25_index.py jfox/search_engine.py tests/unit/test_bm25_concurrency.py`
570
+ Expected: 无错误(black 有差异则 `uv run black <files>` 后重跑测试)
571
+
572
+ - [ ] **Step 3: Commit(如有格式修正)**
573
+
574
+ ```bash
575
+ git add jfox/bm25_index.py jfox/search_engine.py tests/unit/test_bm25_concurrency.py
576
+ git commit -m "chore(bm25): ruff/black 格式修正(#391)"
577
+ ```