code-context-control 2.49.2__tar.gz → 2.50.0__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 (309) hide show
  1. {code_context_control-2.49.2/code_context_control.egg-info → code_context_control-2.50.0}/PKG-INFO +1 -1
  2. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/edit.py +118 -6
  3. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/read.py +22 -4
  4. {code_context_control-2.49.2 → code_context_control-2.50.0/code_context_control.egg-info}/PKG-INFO +1 -1
  5. {code_context_control-2.49.2 → code_context_control-2.50.0}/code_context_control.egg-info/SOURCES.txt +1 -0
  6. {code_context_control-2.49.2 → code_context_control-2.50.0}/pyproject.toml +1 -1
  7. code_context_control-2.50.0/tests/test_read_edit_parity.py +208 -0
  8. {code_context_control-2.49.2 → code_context_control-2.50.0}/LICENSE +0 -0
  9. {code_context_control-2.49.2 → code_context_control-2.50.0}/README.md +0 -0
  10. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/__init__.py +0 -0
  11. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/_hook_utils.py +0 -0
  12. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/c3.py +0 -0
  13. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/commands/__init__.py +0 -0
  14. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/commands/common.py +0 -0
  15. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/commands/parser.py +0 -0
  16. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/docs.html +0 -0
  17. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/edits.html +0 -0
  18. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/guide/bitbucket.html +0 -0
  19. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/guide/getting-started.html +0 -0
  20. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/guide/index.html +0 -0
  21. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/guide/oracle.html +0 -0
  22. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/guide/shared.css +0 -0
  23. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/guide/tools.html +0 -0
  24. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/guide/workflow.html +0 -0
  25. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_artifact.py +0 -0
  26. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_auto_snapshot.py +0 -0
  27. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_c3_signal.py +0 -0
  28. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_c3read.py +0 -0
  29. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_dispatch.py +0 -0
  30. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_edit_ledger.py +0 -0
  31. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_edit_unlock.py +0 -0
  32. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_filter.py +0 -0
  33. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_ghost_files.py +0 -0
  34. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_pretool_enforce.py +0 -0
  35. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_read.py +0 -0
  36. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_session_stats.py +0 -0
  37. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hook_terse_advisor.py +0 -0
  38. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub.html +0 -0
  39. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_server.py +0 -0
  40. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/app.js +0 -0
  41. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/add_project.js +0 -0
  42. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/config_editor.js +0 -0
  43. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/drill_artifacts.js +0 -0
  44. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/drill_health.js +0 -0
  45. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/drill_panel.js +0 -0
  46. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/drill_tasks.js +0 -0
  47. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/drill_views.js +0 -0
  48. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/global_search.js +0 -0
  49. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/mcp_manager.js +0 -0
  50. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/modals.js +0 -0
  51. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/project_card.js +0 -0
  52. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/project_tree.js +0 -0
  53. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/session_drawer.js +0 -0
  54. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/settings_modal.js +0 -0
  55. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/sidebar.js +0 -0
  56. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/summary_bar.js +0 -0
  57. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/task_board.js +0 -0
  58. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/toasts.js +0 -0
  59. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/components/topbar.js +0 -0
  60. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui/state.js +0 -0
  61. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/hub_ui.html +0 -0
  62. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/mcp_proxy.py +0 -0
  63. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/mcp_server.py +0 -0
  64. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/server.py +0 -0
  65. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/__init__.py +0 -0
  66. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/_helpers.py +0 -0
  67. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/agent.py +0 -0
  68. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/artifacts.py +0 -0
  69. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/bitbucket.py +0 -0
  70. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/compress.py +0 -0
  71. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/delegate.py +0 -0
  72. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/edits.py +0 -0
  73. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/federate.py +0 -0
  74. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/filter.py +0 -0
  75. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/impact.py +0 -0
  76. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/memory.py +0 -0
  77. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/project.py +0 -0
  78. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/search.py +0 -0
  79. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/session.py +0 -0
  80. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/shell.py +0 -0
  81. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/status.py +0 -0
  82. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/tasks.py +0 -0
  83. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/tools/validate.py +0 -0
  84. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/api.js +0 -0
  85. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/app.js +0 -0
  86. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/bitbucket.js +0 -0
  87. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/chat.js +0 -0
  88. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/dashboard.js +0 -0
  89. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/edits.js +0 -0
  90. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/instructions.js +0 -0
  91. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/memory.js +0 -0
  92. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/sessions.js +0 -0
  93. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/settings.js +0 -0
  94. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/sidebar.js +0 -0
  95. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/components/tasks.js +0 -0
  96. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/icons.js +0 -0
  97. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/pm_shared.js +0 -0
  98. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/shared.js +0 -0
  99. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui/theme.js +0 -0
  100. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui.html +0 -0
  101. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui_legacy.html +0 -0
  102. {code_context_control-2.49.2 → code_context_control-2.50.0}/cli/ui_nano.html +0 -0
  103. {code_context_control-2.49.2 → code_context_control-2.50.0}/code_context_control.egg-info/dependency_links.txt +0 -0
  104. {code_context_control-2.49.2 → code_context_control-2.50.0}/code_context_control.egg-info/entry_points.txt +0 -0
  105. {code_context_control-2.49.2 → code_context_control-2.50.0}/code_context_control.egg-info/requires.txt +0 -0
  106. {code_context_control-2.49.2 → code_context_control-2.50.0}/code_context_control.egg-info/top_level.txt +0 -0
  107. {code_context_control-2.49.2 → code_context_control-2.50.0}/core/__init__.py +0 -0
  108. {code_context_control-2.49.2 → code_context_control-2.50.0}/core/config.py +0 -0
  109. {code_context_control-2.49.2 → code_context_control-2.50.0}/core/ide.py +0 -0
  110. {code_context_control-2.49.2 → code_context_control-2.50.0}/core/mcp_toml.py +0 -0
  111. {code_context_control-2.49.2 → code_context_control-2.50.0}/core/web_security.py +0 -0
  112. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/__init__.py +0 -0
  113. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/config.py +0 -0
  114. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/mcp_oracle.py +0 -0
  115. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/oracle_server.py +0 -0
  116. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/oracle_ui.html +0 -0
  117. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/__init__.py +0 -0
  118. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/activity_reporter.py +0 -0
  119. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/api_auth.py +0 -0
  120. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/c3_bridge.py +0 -0
  121. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/chat_engine.py +0 -0
  122. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/chat_store.py +0 -0
  123. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/cross_memory.py +0 -0
  124. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/federated_graph.py +0 -0
  125. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/health_checker.py +0 -0
  126. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/insight_engine.py +0 -0
  127. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/local_session.py +0 -0
  128. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/memory_reader.py +0 -0
  129. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/memory_writer.py +0 -0
  130. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/ollama_bridge.py +0 -0
  131. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/project_scanner.py +0 -0
  132. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/review_agent.py +0 -0
  133. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/tool_executor.py +0 -0
  134. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/services/tool_registry.py +0 -0
  135. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/activity.js +0 -0
  136. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/agents.js +0 -0
  137. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/app.js +0 -0
  138. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/busy.js +0 -0
  139. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/chat/conversations.js +0 -0
  140. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/chat/input.js +0 -0
  141. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/chat/markdown.js +0 -0
  142. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/chat/send.js +0 -0
  143. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/chat/stream_renderer.js +0 -0
  144. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/chat/toolbar.js +0 -0
  145. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/core.js +0 -0
  146. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/crossgraph.js +0 -0
  147. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/header.js +0 -0
  148. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/insights.js +0 -0
  149. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/projects.js +0 -0
  150. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/settings.js +0 -0
  151. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/suggestions.js +0 -0
  152. {code_context_control-2.49.2 → code_context_control-2.50.0}/oracle/ui/theme_tabs.js +0 -0
  153. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/__init__.py +0 -0
  154. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/activity_log.py +0 -0
  155. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/agent_base.py +0 -0
  156. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/agents.py +0 -0
  157. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/artifact_defs.py +0 -0
  158. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/artifact_store.py +0 -0
  159. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/auto_memory.py +0 -0
  160. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/bench/__init__.py +0 -0
  161. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/bench/external/__init__.py +0 -0
  162. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/bench/external/aider_polyglot.py +0 -0
  163. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/bench/external/swe_bench.py +0 -0
  164. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/benchmark_dashboard.py +0 -0
  165. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/bitbucket_client.py +0 -0
  166. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/bitbucket_credentials.py +0 -0
  167. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/circuit_breaker.py +0 -0
  168. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/claude_md.py +0 -0
  169. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/compressor.py +0 -0
  170. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/context_snapshot.py +0 -0
  171. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/conversation_store.py +0 -0
  172. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/doc_index.py +0 -0
  173. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/e2e_benchmark.py +0 -0
  174. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/e2e_evaluator.py +0 -0
  175. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/e2e_tasks.py +0 -0
  176. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/edit_ledger.py +0 -0
  177. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/embedding_index.py +0 -0
  178. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/error_reporting.py +0 -0
  179. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/file_memory.py +0 -0
  180. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/git_context.py +0 -0
  181. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/hub_service.py +0 -0
  182. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/indexer.py +0 -0
  183. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/memory.py +0 -0
  184. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/memory_consolidator.py +0 -0
  185. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/memory_graph.py +0 -0
  186. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/memory_grounder.py +0 -0
  187. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/memory_scorer.py +0 -0
  188. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/metrics.py +0 -0
  189. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/notifications.py +0 -0
  190. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/ollama_client.py +0 -0
  191. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/output_filter.py +0 -0
  192. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/parser.py +0 -0
  193. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/project_manager.py +0 -0
  194. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/project_runtime.py +0 -0
  195. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/protocol.py +0 -0
  196. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/proxy_state.py +0 -0
  197. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/retention.py +0 -0
  198. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/retrieval_broker.py +0 -0
  199. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/router.py +0 -0
  200. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/runtime.py +0 -0
  201. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/session_benchmark.py +0 -0
  202. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/session_manager.py +0 -0
  203. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/session_preloader.py +0 -0
  204. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/subprojects.py +0 -0
  205. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/task_store.py +0 -0
  206. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/telemetry.py +0 -0
  207. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/text_index.py +0 -0
  208. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/tool_classifier.py +0 -0
  209. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/transcript_index.py +0 -0
  210. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/validation_cache.py +0 -0
  211. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/vector_store.py +0 -0
  212. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/watcher.py +0 -0
  213. {code_context_control-2.49.2 → code_context_control-2.50.0}/services/win_subprocess.py +0 -0
  214. {code_context_control-2.49.2 → code_context_control-2.50.0}/setup.cfg +0 -0
  215. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_activity_reporter.py +0 -0
  216. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_aider_polyglot.py +0 -0
  217. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_artifact_store.py +0 -0
  218. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_artifact_tool.py +0 -0
  219. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_bitbucket_cli_smoke.py +0 -0
  220. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_bitbucket_client.py +0 -0
  221. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_bitbucket_config_fallback.py +0 -0
  222. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_bitbucket_credentials.py +0 -0
  223. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_bitbucket_tool.py +0 -0
  224. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_c3_bridge_project_artifacts.py +0 -0
  225. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_c3_shell.py +0 -0
  226. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_circuit_breaker.py +0 -0
  227. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_claude_md_merge.py +0 -0
  228. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_cli_smoke.py +0 -0
  229. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_compressor_large_file.py +0 -0
  230. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_delegate_cascade.py +0 -0
  231. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_e2e_benchmark.py +0 -0
  232. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_edit_ledger_hook.py +0 -0
  233. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_edit_normalization.py +0 -0
  234. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_enforcement_flip.py +0 -0
  235. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_federated_graph.py +0 -0
  236. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_filter_backoff.py +0 -0
  237. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_ghost_files.py +0 -0
  238. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_ghost_generation.py +0 -0
  239. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_git_branch_awareness.py +0 -0
  240. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_hook_dispatch.py +0 -0
  241. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_hook_pretool_enforce.py +0 -0
  242. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_hook_smoke.py +0 -0
  243. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_hook_state.py +0 -0
  244. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_hub_inspect_api.py +0 -0
  245. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_hub_server_smoke.py +0 -0
  246. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_install_mcp_entrypoint.py +0 -0
  247. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_lazy_store_init.py +0 -0
  248. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_mcp_host_guard.py +0 -0
  249. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_mcp_server_smoke.py +0 -0
  250. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_mcp_toml.py +0 -0
  251. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_memory_graph_api.py +0 -0
  252. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_memory_system.py +0 -0
  253. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_notification_dedup.py +0 -0
  254. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_notification_discipline.py +0 -0
  255. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_api_auth.py +0 -0
  256. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_apikey_api.py +0 -0
  257. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_c3_bridge.py +0 -0
  258. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_chat_engine.py +0 -0
  259. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_discovery_api.py +0 -0
  260. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_local_auth.py +0 -0
  261. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_ollama_bridge.py +0 -0
  262. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_scanner_cache.py +0 -0
  263. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_security_fixes.py +0 -0
  264. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_subproject_awareness.py +0 -0
  265. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_oracle_ui_bundle.py +0 -0
  266. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_output_filter.py +0 -0
  267. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_permissions.py +0 -0
  268. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_pm_api.py +0 -0
  269. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_project_manager.py +0 -0
  270. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_project_manager_merge.py +0 -0
  271. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_project_tool.py +0 -0
  272. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_read_coercion.py +0 -0
  273. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_response_boilerplate.py +0 -0
  274. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_retention.py +0 -0
  275. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_review_digest.py +0 -0
  276. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_service_durability.py +0 -0
  277. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_session_benchmark.py +0 -0
  278. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_session_budget.py +0 -0
  279. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_shell_robustness.py +0 -0
  280. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_subproject_exclusion.py +0 -0
  281. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_subproject_federation.py +0 -0
  282. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_subprojects.py +0 -0
  283. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_swe_bench.py +0 -0
  284. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_task_store.py +0 -0
  285. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_task_tool.py +0 -0
  286. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_token_telemetry.py +0 -0
  287. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_tool_registry.py +0 -0
  288. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_upgrade_and_version.py +0 -0
  289. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_validate.py +0 -0
  290. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_web_security.py +0 -0
  291. {code_context_control-2.49.2 → code_context_control-2.50.0}/tests/test_windows_reliability.py +0 -0
  292. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/__init__.py +0 -0
  293. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/backend.py +0 -0
  294. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/main.py +0 -0
  295. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/__init__.py +0 -0
  296. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/benchmark_view.py +0 -0
  297. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/claudemd_view.py +0 -0
  298. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/compress_view.py +0 -0
  299. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/index_view.py +0 -0
  300. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/init_view.py +0 -0
  301. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/mcp_view.py +0 -0
  302. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/optimize_view.py +0 -0
  303. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/pipe_view.py +0 -0
  304. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/projects_view.py +0 -0
  305. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/search_view.py +0 -0
  306. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/session_view.py +0 -0
  307. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/stats.py +0 -0
  308. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/screens/ui_view.py +0 -0
  309. {code_context_control-2.49.2 → code_context_control-2.50.0}/tui/theme.tcss +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.49.2
3
+ Version: 2.50.0
4
4
  Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer — and version the configs that shape your agent (CLAUDE.md, skills, hooks, MCP).
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -8,6 +8,7 @@ Parallel safety:
8
8
  - Same file: serialized via per-file threading.Lock (_file_locks).
9
9
  - Same file, multiple hunks: use the `edits` batch parameter — one read/write cycle.
10
10
  """
11
+ import difflib
11
12
  import json
12
13
  import threading
13
14
  from pathlib import Path
@@ -33,12 +34,16 @@ def _read_preserving_newlines(path: Path) -> tuple[str, str]:
33
34
  `newline` is the EOL to write back: ``\r\n`` if CRLF dominates the
34
35
  file, otherwise ``\n``. This avoids Python's text-mode write rewriting
35
36
  every line to ``os.linesep`` on Windows.
37
+
38
+ Decodes with errors="surrogateescape" so files containing non-UTF-8
39
+ bytes are still editable: invalid bytes round-trip losslessly through
40
+ _write_preserving_newlines instead of raising UnicodeDecodeError.
36
41
  """
37
42
  raw = path.read_bytes()
38
43
  crlf = raw.count(b"\r\n")
39
44
  lf_only = raw.count(b"\n") - crlf
40
45
  newline = "\r\n" if crlf > lf_only else "\n"
41
- content = raw.decode("utf-8")
46
+ content = raw.decode("utf-8", errors="surrogateescape")
42
47
  # Normalize to \n internally so replacement matching is EOL-agnostic.
43
48
  content = content.replace("\r\n", "\n").replace("\r", "\n")
44
49
  return content, newline
@@ -52,7 +57,8 @@ def _write_preserving_newlines(path: Path, content: str, newline: str) -> None:
52
57
  """
53
58
  if newline != "\n":
54
59
  content = content.replace("\n", newline)
55
- with open(path, "w", encoding="utf-8", newline="") as fh:
60
+ with open(path, "w", encoding="utf-8", errors="surrogateescape",
61
+ newline="") as fh:
56
62
  fh.write(content)
57
63
 
58
64
 
@@ -66,9 +72,23 @@ _LOOKALIKE_TRANS = str.maketrans({
66
72
  "′": "'", "″": '"', # prime / double prime
67
73
  "‐": "-", "‑": "-", "‒": "-", "–": "-", # hyphen / dashes
68
74
  "—": "-", "―": "-", "−": "-",
69
- " ": " ", "": " ", "": " ", # non-breaking / figure / narrow-nbsp
75
+ "\u00a0": " ", "\u2007": " ", "\u202f": " ", # non-breaking / figure / narrow-nbsp
70
76
  })
71
77
 
78
+ # Bytes that aren't valid UTF-8 decode to lone surrogates U+DC80-U+DCFF under
79
+ # errors="surrogateescape" (how _read_preserving_newlines reads files), while
80
+ # c3_read renders those same bytes as U+FFFD (errors="replace"). Fold both to
81
+ # U+FFFD — 1:1 like the lookalikes — so an old_string copied from c3_read
82
+ # output still matches file content around undecodable bytes.
83
+ _SURROGATE_TRANS = {c: "\ufffd" for c in range(0xDC80, 0xDD00)}
84
+ _LOOKALIKE_TRANS.update(_SURROGATE_TRANS)
85
+
86
+
87
+ def _display_safe(s: str) -> str:
88
+ """Fold surrogateescape'd bytes to U+FFFD for error-message display —
89
+ lone surrogates cannot be encoded for MCP transport."""
90
+ return s.translate(_SURROGATE_TRANS) if s else s
91
+
72
92
 
73
93
  def _norm(s: str) -> str:
74
94
  return s.translate(_LOOKALIKE_TRANS) if s else s
@@ -125,6 +145,81 @@ def _apply_replacement(content: str, old: str, new: str, replace_all: bool):
125
145
  return (_positional_replace(content, nc, no, new, replace_all), count, True)
126
146
 
127
147
 
148
+ def _closest_region(content: str, old: str,
149
+ max_scan_lines: int = 40000, context: int = 2):
150
+ """Locate the file region most similar to a failed old_string.
151
+
152
+ Returns (start_line, end_line, region_text, ratio) — 1-based inclusive
153
+ line numbers, region padded with `context` lines each side — or None when
154
+ no region clears the similarity floor. Powers the 'closest match' payload
155
+ in not-found errors so a mismatched edit can be repaired without
156
+ re-reading the file (the moment agents historically drifted back to
157
+ native Read).
158
+ """
159
+ file_lines = content.split("\n")
160
+ old_lines = old.split("\n")
161
+ n, total = len(old_lines), len(file_lines)
162
+ if not old.strip() or total > max_scan_lines:
163
+ return None
164
+
165
+ # Anchor on old_string's most distinctive line, shortlist file lines that
166
+ # resemble it (cheap quick_ratio pass), then score full windows aligned
167
+ # to each shortlisted line (expensive ratio, capped at 8 candidates).
168
+ anchor_off, anchor_line = max(enumerate(old_lines),
169
+ key=lambda p: len(p[1].strip()))
170
+ anchor = anchor_line.strip()
171
+ sm = difflib.SequenceMatcher(autojunk=False)
172
+ sm.set_seq2(anchor)
173
+ scored = []
174
+ for i, line in enumerate(file_lines):
175
+ sm.set_seq1(line.strip())
176
+ if sm.real_quick_ratio() < 0.6:
177
+ continue
178
+ q = sm.quick_ratio()
179
+ if q >= 0.6:
180
+ scored.append((q, i))
181
+ if not scored:
182
+ return None
183
+ scored.sort(key=lambda t: -t[0])
184
+
185
+ win = difflib.SequenceMatcher(autojunk=False)
186
+ win.set_seq2(old)
187
+ best_ratio, best_start = 0.0, None
188
+ for _, i in scored[:8]:
189
+ start = max(0, min(i - anchor_off, total - n))
190
+ win.set_seq1("\n".join(file_lines[start:start + n]))
191
+ r = win.ratio()
192
+ if r > best_ratio:
193
+ best_ratio, best_start = r, start
194
+ if best_start is None or best_ratio < 0.5:
195
+ return None
196
+ lo = max(0, best_start - context)
197
+ hi = min(total, best_start + n + context)
198
+ return lo + 1, hi, "\n".join(file_lines[lo:hi]), best_ratio
199
+
200
+
201
+ _REGION_CAP = 4000
202
+
203
+
204
+ def _not_found_payload(near, file_label: str) -> str:
205
+ """Render a _closest_region result as an error-message appendix. The
206
+ region text is emitted verbatim (no indentation) so it can be copied
207
+ straight into a retry old_string."""
208
+ if not near:
209
+ return ""
210
+ lo, hi, region, ratio = near
211
+ region = _display_safe(region)
212
+ if len(region) > _REGION_CAP:
213
+ region = (region[:_REGION_CAP]
214
+ + f"\n⟦trimmed — run c3_read(file_path='{file_label}', "
215
+ f"lines=[{lo},{hi}]) for the rest⟧")
216
+ return (f"\n closest match: L{lo}-L{hi} ({ratio:.0%} similar). "
217
+ f"Actual file text between the markers:\n"
218
+ f"⟦L{lo}-L{hi}⟧\n{region}\n⟦end⟧\n"
219
+ f" Retry with old_string copied exactly from the text above "
220
+ f"(markers excluded) — no need to re-read the file.")
221
+
222
+
128
223
  def handle_edit(file_path: str, old_string: str, new_string: str,
129
224
  summary: str, tags: str, replace_all: bool,
130
225
  svc, finalize, edits: str = "") -> str:
@@ -209,8 +304,10 @@ def handle_edit(file_path: str, old_string: str, new_string: str,
209
304
  f"Read error: {e}", "read error")
210
305
 
211
306
  results = []
307
+ statuses = [] # parallel to results: 'ok' | 'miss' | 'ambiguous' | 'skipped'
212
308
  any_normalized = False
213
309
  any_applied = False
310
+ first_miss = ""
214
311
  for i, patch in enumerate(edit_list):
215
312
  old = patch.get("old_string", "")
216
313
  new = patch.get("new_string", "")
@@ -219,15 +316,23 @@ def handle_edit(file_path: str, old_string: str, new_string: str,
219
316
 
220
317
  if not old:
221
318
  results.append(f" patch[{i}]: skipped — empty old_string")
319
+ statuses.append("skipped")
222
320
  continue
223
321
 
224
322
  new_content, count, used_fallback = _apply_replacement(content, old, new, r_all)
225
323
  if new_content is None and count == 0:
226
- results.append(f" patch[{i}]: NOT FOUND — {old[:80]!r}")
324
+ near = _closest_region(content, old)
325
+ loc = (f" (closest: L{near[0]}-L{near[1]}, {near[3]:.0%} similar)"
326
+ if near else "")
327
+ results.append(f" patch[{i}]: NOT FOUND — {old[:80]!r}{loc}")
328
+ statuses.append("miss")
329
+ if near and not first_miss:
330
+ first_miss = _not_found_payload(near, file_path)
227
331
  continue
228
332
  if new_content is None:
229
333
  tag = " (unicode-normalized)" if used_fallback else ""
230
334
  results.append(f" patch[{i}]: AMBIGUOUS ({count} matches){tag} — {old[:60]!r}")
335
+ statuses.append("ambiguous")
231
336
  continue
232
337
 
233
338
  content = new_content
@@ -243,6 +348,7 @@ def handle_edit(file_path: str, old_string: str, new_string: str,
243
348
  + (f" ({n}x)" if n > 1 else "")
244
349
  + (" [norm]" if used_fallback else "")
245
350
  + f" | {desc}")
351
+ statuses.append("ok")
246
352
 
247
353
  # Only touch the file when at least one patch actually changed it —
248
354
  # avoids rewriting (and re-EOL-normalizing) an unchanged file and
@@ -266,12 +372,17 @@ def handle_edit(file_path: str, old_string: str, new_string: str,
266
372
  ]}
267
373
  _log_to_ledger(rel, summary or f"Batch edit: {len(edit_list)} patches", tag_list, svc, detail=batch_detail)
268
374
 
269
- applied = sum(1 for r in results if "NOT FOUND" not in r and "AMBIGUOUS" not in r and "skipped" not in r)
375
+ # Classify from the structured status list, not by substring-scanning
376
+ # the human-readable result lines — a patch *summary* containing words
377
+ # like 'NOT FOUND' used to be miscounted as a failure.
378
+ applied = statuses.count("ok")
270
379
  norm_tag = " [unicode-normalized]" if any_normalized else ""
271
380
  short = f"✓ {rel} — {applied}/{len(edit_list)} patches applied{norm_tag}"
272
381
  if applied < len(edit_list):
273
- failed = [r for r in results if "NOT FOUND" in r or "AMBIGUOUS" in r or "skipped" in r]
382
+ failed = [r for r, s in zip(results, statuses) if s != "ok"]
274
383
  short += "\n" + "\n".join(failed)
384
+ if first_miss:
385
+ short += first_miss
275
386
  return finalize("c3_edit", {"file": file_path}, short,
276
387
  f"{rel} patched ({len(edit_list)} patches)")
277
388
 
@@ -293,6 +404,7 @@ def handle_edit(file_path: str, old_string: str, new_string: str,
293
404
  hint = ""
294
405
  if _norm(old_string) != old_string or _norm(content) != content:
295
406
  hint = "\n hint: unicode-lookalike normalization also failed to match."
407
+ hint += _not_found_payload(_closest_region(content, old_string), file_path)
296
408
  return finalize("c3_edit", {"file": file_path},
297
409
  f"old_string not found in {file_path}\n"
298
410
  f" searched for: {old_string[:120]!r}{hint}",
@@ -172,7 +172,14 @@ def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
172
172
  pass
173
173
 
174
174
  raw_text = full.read_text(encoding="utf-8", errors="replace")
175
- content_lines = raw_text.splitlines()
175
+ # EOL-normalize exactly the way c3_edit's matcher does (\r\n and \r → \n),
176
+ # then split on \n ONLY. splitlines() also breaks on \x0c/\u2028/\x85 etc.,
177
+ # which rendered those in-line chars as line breaks — an old_string copied
178
+ # from that output (with \n) could never match the actual file bytes.
179
+ raw_text = raw_text.replace("\r\n", "\n").replace("\r", "\n")
180
+ content_lines = raw_text.split("\n")
181
+ if content_lines and content_lines[-1] == "":
182
+ content_lines.pop() # trailing \n would otherwise add a phantom empty line
176
183
  # Lazy: only count full file tokens when needed for the summary string
177
184
  _full_tok_cache = [None]
178
185
 
@@ -254,8 +261,9 @@ def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
254
261
 
255
262
  if not ranges:
256
263
  file_map = svc.file_memory.get_or_build_map(rel_path)
257
- map_tok = count_tokens(file_map)
258
- resp = file_map
264
+ resp = (file_map
265
+ + "\n[map only — pass lines=[start,end] or symbols=[...] for exact source]")
266
+ map_tok = count_tokens(resp)
259
267
  return finalize_with_tokens(
260
268
  finalize, svc, "c3_read", {"file": file_path},
261
269
  resp, f"{full_file_tokens()}->{map_tok}tok",
@@ -278,13 +286,23 @@ def handle_read(file_path: str, symbols: Any = None, lines: Any = None,
278
286
  header = f"[read:{file_path}]"
279
287
 
280
288
  parts = []
289
+ prev_end = None
281
290
  for start, end in ranges:
282
291
  s_idx = max(0, start - 1)
283
292
  e_idx = min(len(content_lines), end)
284
293
  chunk = content_lines[s_idx:e_idx]
285
294
  if len(ranges) > 1:
286
- parts.append(f"--- L{start}-L{end} ---")
295
+ # ⟦…⟧ markers are tool chrome, not file content. The gap note makes
296
+ # the discontinuity explicit so a copied old_string never spans it.
297
+ if prev_end is None:
298
+ parts.append(f"⟦L{start}-L{end}⟧")
299
+ else:
300
+ parts.append(
301
+ f"⟦L{start}-L{end} — {start - prev_end - 1} lines "
302
+ f"(L{prev_end + 1}-L{start - 1}) omitted; blocks are NOT "
303
+ f"contiguous, never span this marker in a c3_edit old_string⟧")
287
304
  parts.extend(chunk)
305
+ prev_end = end
288
306
 
289
307
  final_content = "\n".join(parts)
290
308
  tokens = count_tokens(final_content)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-context-control
3
- Version: 2.49.2
3
+ Version: 2.50.0
4
4
  Summary: Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer — and version the configs that shape your agent (CLAUDE.md, skills, hooks, MCP).
5
5
  Author-email: Dimitri Tselenchuk <dtselenc@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -267,6 +267,7 @@ tests/test_project_manager.py
267
267
  tests/test_project_manager_merge.py
268
268
  tests/test_project_tool.py
269
269
  tests/test_read_coercion.py
270
+ tests/test_read_edit_parity.py
270
271
  tests/test_response_boilerplate.py
271
272
  tests/test_retention.py
272
273
  tests/test_review_digest.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code-context-control"
7
- version = "2.49.2"
7
+ version = "2.50.0"
8
8
  description = "Local code-intelligence layer for AI coding tools (Claude Code, Codex, Gemini, Copilot). Retrieve less, read less, edit safer — and version the configs that shape your agent (CLAUDE.md, skills, hooks, MCP)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,208 @@
1
+ """Parity tests between c3_read output and c3_edit matching.
2
+
3
+ Regression suite for the read→edit drift bug: old_strings copied from
4
+ c3_read output failed to match in c3_edit (encoding asymmetry, splitlines()
5
+ unicode line-boundary splitting, separator pollution), pushing agents back
6
+ to native Read/Edit. These tests pin the unified pipeline:
7
+
8
+ - read: EOL-only normalization, form feeds/U+2028 stay inline,
9
+ copy-safe ⟦…⟧ range markers with explicit gap notes, map-hint footer.
10
+ - edit: surrogateescape round-trip for non-UTF-8 bytes, U+FFFD match
11
+ folding, closest-region payload on not-found.
12
+ """
13
+ import sys
14
+ import tempfile
15
+ import unittest
16
+ from pathlib import Path
17
+ from unittest.mock import MagicMock
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
20
+
21
+ from cli.tools.edit import _closest_region, handle_edit # noqa: E402
22
+ from cli.tools.read import handle_read # noqa: E402
23
+
24
+
25
+ def _make_svc(project_path: Path):
26
+ svc = MagicMock()
27
+ svc.project_path = str(project_path)
28
+ svc.edit_ledger = None
29
+ svc.activity_log = None
30
+ svc.session_mgr = None
31
+ return svc
32
+
33
+
34
+ def _finalize(name, args, resp, summ, **kw):
35
+ return resp
36
+
37
+
38
+ class _Base(unittest.TestCase):
39
+ def setUp(self):
40
+ self.tmp = tempfile.TemporaryDirectory()
41
+ self.root = Path(self.tmp.name)
42
+ self.svc = _make_svc(self.root)
43
+
44
+ def tearDown(self):
45
+ self.tmp.cleanup()
46
+
47
+ def _write_text(self, rel: str, text: str) -> Path:
48
+ p = self.root / rel
49
+ p.write_text(text, encoding="utf-8", newline="")
50
+ return p
51
+
52
+ def _write_bytes(self, rel: str, data: bytes) -> Path:
53
+ p = self.root / rel
54
+ p.write_bytes(data)
55
+ return p
56
+
57
+ def _read(self, rel: str, **kw) -> str:
58
+ return handle_read(rel, svc=self.svc, finalize=_finalize, **kw)
59
+
60
+ def _edit(self, rel: str, old: str, new: str, **kw) -> str:
61
+ return handle_edit(rel, old, new, summary="", tags="",
62
+ replace_all=kw.pop("replace_all", False),
63
+ svc=self.svc, finalize=_finalize, **kw)
64
+
65
+
66
+ class TestReadEolParity(_Base):
67
+ def test_form_feed_stays_inline(self):
68
+ # splitlines() used to break on \x0c, showing a phantom line break
69
+ # that made copied old_strings unmatchable.
70
+ self._write_text("a.py", "alpha\x0cbeta\ngamma\n")
71
+ resp = self._read("a.py", lines=[1, 2])
72
+ self.assertIn("alpha\x0cbeta", resp)
73
+ self.assertIn("gamma", resp)
74
+
75
+ def test_u2028_stays_inline(self):
76
+ self._write_text("b.py", "one\u2028two\nthree\n")
77
+ resp = self._read("b.py", lines=[1, 2])
78
+ self.assertIn("one\u2028two", resp)
79
+
80
+ def test_crlf_normalized_to_lf(self):
81
+ self._write_bytes("c.py", b"line1\r\nline2\r\n")
82
+ resp = self._read("c.py", lines=[1, 2])
83
+ self.assertNotIn("\r", resp)
84
+ self.assertIn("line1\nline2", resp)
85
+
86
+ def test_read_snippet_roundtrips_through_edit(self):
87
+ # The core parity guarantee: any snippet of c3_read output (single
88
+ # range) must match verbatim as a c3_edit old_string.
89
+ self._write_bytes("d.py", b"def f():\r\n return 1\r\n")
90
+ resp = self._read("d.py", lines=[1, 2])
91
+ self.assertIn("def f():\n return 1", resp)
92
+ edit_resp = self._edit("d.py", "def f():\n return 1",
93
+ "def f():\n return 2")
94
+ self.assertIn("✓", edit_resp) # ✓
95
+ self.assertEqual((self.root / "d.py").read_bytes(),
96
+ b"def f():\r\n return 2\r\n")
97
+
98
+
99
+ class TestReadRangeMarkers(_Base):
100
+ def setUp(self):
101
+ super().setUp()
102
+ self._write_text("m.py",
103
+ "".join(f"line{i}\n" for i in range(1, 13)))
104
+
105
+ def test_multi_range_has_gap_note(self):
106
+ resp = self._read("m.py", lines=[[1, 2], [8, 9]])
107
+ self.assertIn("⟦L1-L2⟧", resp) # ⟦L1-L2⟧
108
+ self.assertIn("omitted", resp)
109
+ self.assertIn("L3-L7", resp) # the gap range
110
+ self.assertNotIn("--- L", resp) # old separator gone
111
+
112
+ def test_single_range_has_no_marker(self):
113
+ resp = self._read("m.py", lines=[1, 3])
114
+ self.assertNotIn("⟦", resp)
115
+
116
+
117
+ class TestReadMapHint(_Base):
118
+ def test_map_response_says_how_to_get_source(self):
119
+ self._write_text("n.py", "x = 1\n")
120
+ self.svc.file_memory.get_or_build_map.return_value = "FILE MAP"
121
+ resp = self._read("n.py")
122
+ self.assertIn("FILE MAP", resp)
123
+ self.assertIn("map only", resp)
124
+ self.assertIn("lines=[start,end]", resp)
125
+
126
+
127
+ class TestEditByteSafety(_Base):
128
+ LATIN1 = b"# caf\xe9 latin-1 comment\nvalue = 1\n"
129
+
130
+ def test_edit_succeeds_despite_invalid_utf8(self):
131
+ # Strict decode used to raise UnicodeDecodeError before matching.
132
+ self._write_bytes("e.py", self.LATIN1)
133
+ resp = self._edit("e.py", "value = 1", "value = 2")
134
+ self.assertIn("✓", resp)
135
+
136
+ def test_untouched_invalid_bytes_roundtrip(self):
137
+ self._write_bytes("e.py", self.LATIN1)
138
+ self._edit("e.py", "value = 1", "value = 2")
139
+ data = (self.root / "e.py").read_bytes()
140
+ self.assertIn(b"caf\xe9", data) # byte preserved, not U+FFFD'd
141
+ self.assertIn(b"value = 2", data)
142
+
143
+ def test_fffd_from_read_output_matches_raw_byte(self):
144
+ # c3_read renders \xe9 as U+FFFD; that old_string must still match.
145
+ self._write_bytes("e.py", self.LATIN1)
146
+ resp = self._edit("e.py", "# caf� latin-1 comment", "# cafe comment")
147
+ self.assertIn("✓", resp)
148
+ self.assertNotIn(b"\xe9", (self.root / "e.py").read_bytes())
149
+
150
+
151
+ class TestEditClosestRegion(_Base):
152
+ SRC = ("def foo(a, b):\n"
153
+ " return compute(a, b)\n"
154
+ "\n"
155
+ "def bar():\n"
156
+ " return 2\n")
157
+
158
+ def test_not_found_includes_closest_region(self):
159
+ self._write_text("f.py", self.SRC)
160
+ resp = self._edit("f.py", " return compute(a, c)", " return 0")
161
+ self.assertIn("not found", resp)
162
+ self.assertIn("closest match", resp)
163
+ self.assertIn("return compute(a, b)", resp) # actual file text
164
+ self.assertIn("no need to re-read", resp)
165
+
166
+ def test_wildly_wrong_old_string_gets_no_region(self):
167
+ self._write_text("f.py", self.SRC)
168
+ resp = self._edit("f.py", "zzz@@qq##www!!", "x")
169
+ self.assertIn("not found", resp)
170
+ self.assertNotIn("closest match", resp)
171
+
172
+ def test_batch_miss_carries_locator_and_region(self):
173
+ self._write_text("f.py", self.SRC)
174
+ resp = handle_edit(
175
+ "f.py", "", "", summary="", tags="", replace_all=False,
176
+ svc=self.svc, finalize=_finalize,
177
+ edits='[{"old_string":" return 2","new_string":" return 3"},'
178
+ '{"old_string":" return compute(a, c)","new_string":" return 0"}]',
179
+ )
180
+ self.assertIn("1/2 patches applied", resp)
181
+ self.assertIn("NOT FOUND", resp)
182
+ self.assertIn("closest:", resp)
183
+ self.assertIn("return compute(a, b)", resp)
184
+
185
+ def test_batch_summary_words_not_miscounted(self):
186
+ # Regression: patch summaries containing 'NOT FOUND'/'skipped' were
187
+ # substring-matched by the outcome classifier and reported as failures.
188
+ self._write_text("f.py", self.SRC)
189
+ resp = handle_edit(
190
+ "f.py", "", "", summary="", tags="", replace_all=False,
191
+ svc=self.svc, finalize=_finalize,
192
+ edits='[{"old_string":" return 2","new_string":" return 3",'
193
+ '"summary":"this mentions NOT FOUND and skipped"}]',
194
+ )
195
+ self.assertIn("1/1 patches applied", resp)
196
+
197
+ def test_closest_region_line_numbers(self):
198
+ near = _closest_region(self.SRC, " return compute(a, c)")
199
+ self.assertIsNotNone(near)
200
+ lo, hi, region, ratio = near
201
+ self.assertLessEqual(lo, 2)
202
+ self.assertGreaterEqual(hi, 2)
203
+ self.assertIn("return compute(a, b)", region)
204
+ self.assertGreater(ratio, 0.8)
205
+
206
+
207
+ if __name__ == "__main__":
208
+ unittest.main()