bantamkit 0.29.2__tar.gz → 0.30.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 (207) hide show
  1. {bantamkit-0.29.2 → bantamkit-0.30.0}/PKG-INFO +2 -2
  2. bantamkit-0.30.0/_assets/tools/memory_dream.json +20 -0
  3. bantamkit-0.30.0/_assets/tools/repo_map.json +44 -0
  4. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/__init__.py +1 -1
  5. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/eventlog.py +5 -4
  6. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/mcpserver.py +169 -2
  7. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/__init__.py +15 -1
  8. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/__main__.py +1 -1
  9. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/component.py +206 -4
  10. bantamkit-0.30.0/src/bantamkit/memory/dream.py +751 -0
  11. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/store.py +94 -3
  12. bantamkit-0.30.0/src/bantamkit/repomap.py +1026 -0
  13. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/served-tool-surface.json +68 -1
  14. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_bantamkit_read_tool.py +2 -0
  15. bantamkit-0.30.0/tests/test_conformance_suite_table_gate.py +124 -0
  16. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_eventlog.py +4 -2
  17. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mcpserver.py +11 -5
  18. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory.py +168 -0
  19. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_compact_tool.py +3 -1
  20. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_component.py +72 -1
  21. bantamkit-0.30.0/tests/test_memory_dream.py +875 -0
  22. bantamkit-0.30.0/tests/test_repo_map_tool.py +296 -0
  23. bantamkit-0.30.0/tests/test_repomap.py +649 -0
  24. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_served_tool_count_records.py +98 -14
  25. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_skillaudit.py +6 -1
  26. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_status_surface.py +1 -1
  27. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_tool_manifest.py +2 -0
  28. {bantamkit-0.29.2 → bantamkit-0.30.0}/.gitignore +0 -0
  29. {bantamkit-0.29.2 → bantamkit-0.30.0}/README.md +0 -0
  30. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/contracts/default.yaml +0 -0
  31. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/manifest.yaml +0 -0
  32. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/HISTORY.md +0 -0
  33. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/README.md +0 -0
  34. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/docs/architecture.md +0 -0
  35. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/docs/runbook.md +0 -0
  36. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/issues/142-settlement-timeout.md +0 -0
  37. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/patches/0009-retry-budget.patch +0 -0
  38. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/__init__.py +0 -0
  39. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/config.py +0 -0
  40. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/errors.py +0 -0
  41. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/posting.py +0 -0
  42. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/registry.py +0 -0
  43. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/report.py +0 -0
  44. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/retry.py +0 -0
  45. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/settle.py +0 -0
  46. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/src/ledger/validate.py +0 -0
  47. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/tests/test_posting.py +0 -0
  48. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/repo/tests/test_settle.py +0 -0
  49. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-error-contract.yaml +0 -0
  50. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-handler-map.yaml +0 -0
  51. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-patch-before-after.yaml +0 -0
  52. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-retry-attempts.yaml +0 -0
  53. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-settlement-config.yaml +0 -0
  54. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-symbol-home.yaml +0 -0
  55. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-trace-blame.yaml +0 -0
  56. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/devteam/tasks/dt-unread-key.yaml +0 -0
  57. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-in-137.yaml +0 -0
  58. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-in-359.yaml +0 -0
  59. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-in-372.yaml +0 -0
  60. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-out-11764.yaml +0 -0
  61. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-out-4137.yaml +0 -0
  62. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-large-out-8022.yaml +0 -0
  63. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-small-137.yaml +0 -0
  64. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-small-261.yaml +0 -0
  65. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/document/tasks/doc-small-388.yaml +0 -0
  66. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/fixtures/.gitkeep +0 -0
  67. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/fixtures/catalog.json +0 -0
  68. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/perturbations/task-completion.yaml +0 -0
  69. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/.gitkeep +0 -0
  70. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-contact.yaml +0 -0
  71. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-invoice.yaml +0 -0
  72. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-order.yaml +0 -0
  73. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-schedule.yaml +0 -0
  74. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/extract-versions.yaml +0 -0
  75. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/nav-prod-port.yaml +0 -0
  76. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/nav-release-bundle.yaml +0 -0
  77. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-audit-retention.yaml +0 -0
  78. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-cache-ttl.yaml +0 -0
  79. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-db-port.yaml +0 -0
  80. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-deploy.yaml +0 -0
  81. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-env-endpoint.yaml +0 -0
  82. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-oncall-rotation.yaml +0 -0
  83. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-oncall.yaml +0 -0
  84. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-org-quota.yaml +0 -0
  85. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/recall-owner.yaml +0 -0
  86. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-basket-total.yaml +0 -0
  87. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-cheapest.yaml +0 -0
  88. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-compare.yaml +0 -0
  89. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-gadget-value.yaml +0 -0
  90. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-stock-total.yaml +0 -0
  91. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/evals/tasks/shop-total.yaml +0 -0
  92. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/profiles/default.yaml +0 -0
  93. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/profiles/patient.yaml +0 -0
  94. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/rubrics/.gitkeep +0 -0
  95. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/rubrics/code-quality.yaml +0 -0
  96. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/rubrics/grounded-completion.yaml +0 -0
  97. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/rubrics/task-completion.yaml +0 -0
  98. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/schemas/shiftwork-checkpoint.json +0 -0
  99. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/skills/.gitkeep +0 -0
  100. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/skills/file-graph.md +0 -0
  101. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/skills/memory.md +0 -0
  102. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/.gitkeep +0 -0
  103. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/bantamkit_read.json +0 -0
  104. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/bantamkit_status.json +0 -0
  105. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/build_identity.json +0 -0
  106. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/document_list.json +0 -0
  107. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/document_read.json +0 -0
  108. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/file_graph.json +0 -0
  109. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/memory_compact.json +0 -0
  110. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/memory_recall.json +0 -0
  111. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/memory_save.json +0 -0
  112. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/shiftwork_clock_in.json +0 -0
  113. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/shiftwork_clock_out.json +0 -0
  114. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/shiftwork_status.json +0 -0
  115. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/skill_audit.json +0 -0
  116. {bantamkit-0.29.2 → bantamkit-0.30.0}/_assets/tools/validate_json.json +0 -0
  117. {bantamkit-0.29.2 → bantamkit-0.30.0}/hatch_build.py +0 -0
  118. {bantamkit-0.29.2 → bantamkit-0.30.0}/pyproject.toml +0 -0
  119. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/agent.py +0 -0
  120. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/assets.py +0 -0
  121. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/budget.py +0 -0
  122. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/client.py +0 -0
  123. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/contract.py +0 -0
  124. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/criticreplay.py +0 -0
  125. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/critique.py +0 -0
  126. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/docmanifest.py +0 -0
  127. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/docread.py +0 -0
  128. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/evalrun.py +0 -0
  129. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/filegraph.py +0 -0
  130. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/hostinstall.py +0 -0
  131. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/loopguard.py +0 -0
  132. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/mcpreport.py +0 -0
  133. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/divergence.py +0 -0
  134. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/memory/layers.py +0 -0
  135. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/pdfread.py +0 -0
  136. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/profile.py +0 -0
  137. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/shiftwork.py +0 -0
  138. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/skillaudit.py +0 -0
  139. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/statusline.py +0 -0
  140. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/structured.py +0 -0
  141. {bantamkit-0.29.2 → bantamkit-0.30.0}/src/bantamkit/textutil.py +0 -0
  142. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/cli_exit_status_probe.py +0 -0
  143. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/conftest.py +0 -0
  144. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/bad-crc.docx +0 -0
  145. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/charref-4301-digits.html +0 -0
  146. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/charset-table.json +0 -0
  147. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/compression-method-9.docx +0 -0
  148. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/corrupt-deflate.docx +0 -0
  149. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/encrypted-member.docx +0 -0
  150. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/encrypted-mimetype.odt +0 -0
  151. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/eszett-cell-ref.xlsx +0 -0
  152. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/internal-dtd-entity.docx +0 -0
  153. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/rfc2231-charset.eml +0 -0
  154. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/rfc822-nested-twice.eml +0 -0
  155. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/unicode-digit-shared-string.xlsx +0 -0
  156. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/docread/x-uuencode.eml +0 -0
  157. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/f8404ab-perturbation-baseline.json +0 -0
  158. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/data/platform-assumption-baseline.json +0 -0
  159. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/docread_fixtures.py +0 -0
  160. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/perturbation_baseline_harness.py +0 -0
  161. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/rbp16_effect_probe.py +0 -0
  162. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/rbp18_payload_probe.py +0 -0
  163. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_adapter.py +0 -0
  164. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_agent.py +0 -0
  165. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_amendguard.py +0 -0
  166. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_budget.py +0 -0
  167. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_build_identity.py +0 -0
  168. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_client.py +0 -0
  169. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_compaction_corpus_survey.py +0 -0
  170. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_conformance.py +0 -0
  171. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_contract_fanout.py +0 -0
  172. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_criticreplay.py +0 -0
  173. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_critique.py +0 -0
  174. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_doc_commands_gate.py +0 -0
  175. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_docread.py +0 -0
  176. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_docread_ceilings.py +0 -0
  177. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_document_manifest_parity.py +0 -0
  178. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_document_setup.py +0 -0
  179. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_document_tasks.py +0 -0
  180. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_document_tools.py +0 -0
  181. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_encoding_gate.py +0 -0
  182. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_evalrun.py +0 -0
  183. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_field_program_gates.py +0 -0
  184. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_field_programs.py +0 -0
  185. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_filegraph.py +0 -0
  186. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_hostinstall.py +0 -0
  187. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_ladder_statistics.py +0 -0
  188. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_launcher_which.py +0 -0
  189. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_layers.py +0 -0
  190. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_loopguard.py +0 -0
  191. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mcp_endpoint.py +0 -0
  192. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mcpdrift.py +0 -0
  193. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mcpreport.py +0 -0
  194. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_divergence.py +0 -0
  195. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_layers.py +0 -0
  196. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_memory_store_tripwire.py +0 -0
  197. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_mutmatrix.py +0 -0
  198. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_newline_gate.py +0 -0
  199. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_packaging.py +0 -0
  200. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_pdfread.py +0 -0
  201. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_pinharness_ledger.py +0 -0
  202. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_platform_assumption_gate.py +0 -0
  203. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_shiftwork.py +0 -0
  204. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_statusline.py +0 -0
  205. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_structured.py +0 -0
  206. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_thread_exception_gate.py +0 -0
  207. {bantamkit-0.29.2 → bantamkit-0.30.0}/tests/test_version_agreement.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: bantamkit
3
- Version: 0.29.2
3
+ Version: 0.30.0
4
4
  Summary: bantamweight tooling — harness primitives that lift small-model agents
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "memory_dream",
3
+ "description": "Consolidate the memories the project layer and the machine-wide profile layer hold under the SAME NAME, and say exactly what changed. Byte-identical copies collapse to one; a pair that has diverged is UNIONED — every claim from both copies survives, never a winner-takes-all — and a relative date in a body is annotated with the day it resolves to against that fact's own mtime, never against today. The survivor stays in the writable project layer and the profile copy MOVES to that store's archive/: nothing is deleted and any consumed memory can be restored by name. This is a correctness pass, not a token saving: the profile index is derived at read time and is not loaded from a prompt, so consolidating it frees approximately no bytes — what it buys is one copy of a ruling instead of two that can disagree. It defaults to a DRY RUN because the profile store is machine-wide: a memory archived out of it stops answering for every other project on this machine that has no store of its own. Read the plan first, then call it again with dry_run false to apply it.",
4
+ "surfaces": ["mcp"],
5
+ "parameters": {
6
+ "type": "object",
7
+ "properties": {
8
+ "dry_run": {
9
+ "type": "boolean",
10
+ "description": "Preview only. True (the default) performs every read, merge and projection and writes nothing. Pass false to apply the plan."
11
+ }
12
+ }
13
+ },
14
+ "output_schema": {
15
+ "properties": { "result": { "title": "Result", "type": "string" } },
16
+ "required": ["result"],
17
+ "type": "object",
18
+ "title": "memory_dreamOutput"
19
+ }
20
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "repo_map",
3
+ "description": "A ranked map of a source tree: every file's definitions, ordered by how central that file is to the files you name in `focus`, truncated to a byte budget. Call it when you are about to work on a file and want to know which OTHER files matter to it — the ports, the callers, the module it reaches through a private helper — rather than reading a directory listing and guessing. `focus` is the file or files you are editing, relative to `root` and POSIX-separated; they are excluded from the listing because you already have them open, and an empty focus gives plain centrality over the whole tree. This is a PRECISION pass and it is not a token saving: the gate this feature was supposed to clear was refuted by measurement — discovery is 0.114% of real prompt tokens, because 97.8% of the bill is cache_read — so a map does not make a session cheaper. What it buys is the right file found sooner. Nothing the scanner cannot read is dropped silently: every unread file resolves into a named omission (unknown-language, unreadable-bytes, size-cap, no-definitions, unreachable, per-file-cap, budget) counted in a footer that is NOT charged to the budget. The budget is UTF-8 BYTES of the listing, not tokens; there is no model tokenizer in either runtime and this tool will not pretend to one.",
4
+ "surfaces": [
5
+ "mcp"
6
+ ],
7
+ "parameters": {
8
+ "type": "object",
9
+ "required": [
10
+ "root"
11
+ ],
12
+ "properties": {
13
+ "root": {
14
+ "type": "string",
15
+ "description": "Directory to map, absolute or relative to the server's working directory"
16
+ },
17
+ "focus": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "string"
21
+ },
22
+ "description": "The files you are working on, relative to `root` and POSIX-separated. They are excluded from the listing. A name that is not a scanned source is ignored rather than refused."
23
+ },
24
+ "budget": {
25
+ "type": "integer",
26
+ "minimum": 0,
27
+ "description": "UTF-8 bytes of listing to render; default 4000. The omission footer is not charged against it."
28
+ }
29
+ }
30
+ },
31
+ "output_schema": {
32
+ "properties": {
33
+ "result": {
34
+ "title": "Result",
35
+ "type": "string"
36
+ }
37
+ },
38
+ "required": [
39
+ "result"
40
+ ],
41
+ "type": "object",
42
+ "title": "repo_mapOutput"
43
+ }
44
+ }
@@ -29,4 +29,4 @@ from bantamkit.structured import StructuredOutputError, extract_json, structured
29
29
  # file as its dynamic version source, so the wheel's metadata and the string the MCP
30
30
  # server advertises are the same committed bytes, and neither is a function of when
31
31
  # someone last ran `pip`.
32
- __version__ = "0.29.2"
32
+ __version__ = "0.30.0"
@@ -30,10 +30,11 @@ THREE HARD RULES, each with the failure it prevents:
30
30
  byte-compares both runtimes' streams; one stray write breaks the wire suite. Nothing
31
31
  in this module touches `sys.stderr` or `sys.stdout`.
32
32
  * **Metadata only.** Never a tool argument's value, never a memory body, never a
33
- validated output, never a query, never a document row. Six of the eleven tools take
34
- unbounded free text and five take absolute paths (`bantamkit_read`'s `path` is one and
35
- `skill_audit`'s `root` is another; each record carries counts and tokens from a closed
36
- set, never the path, never a part name, never a skill id).
33
+ validated output, never a query, never a document row. Seven of the thirteen tools take
34
+ unbounded free text and six take absolute paths (`bantamkit_read`'s `path` is one,
35
+ `skill_audit`'s `root` is another and `repo_map`'s `root` and `focus` are the third;
36
+ each record carries counts and tokens from a closed set, never the path, never a part
37
+ name, never a skill id, never a mapped file).
37
38
  Every value written here is an ASCII token from a closed
38
39
  set, an `int`, or a `bool`.
39
40
  * **Never `str(exception)`.** Only `type(exc).__name__`. This is not hypothetical: the
@@ -17,7 +17,15 @@ from pathlib import Path
17
17
  from typing import Annotated, Any
18
18
 
19
19
  import bantamkit
20
- from bantamkit import __version__, docmanifest, docread, hostinstall, shiftwork, skillaudit
20
+ from bantamkit import (
21
+ __version__,
22
+ docmanifest,
23
+ docread,
24
+ hostinstall,
25
+ repomap,
26
+ shiftwork,
27
+ skillaudit,
28
+ )
21
29
  from bantamkit.assets import AssetNotFound, assets_root, load_skill, load_tool_asset
22
30
  from bantamkit.client import BantamError
23
31
  from bantamkit.contract import (
@@ -861,6 +869,59 @@ def _record_result(log: EventLog, tool: str, call: Callable[[], dict[str, Any]])
861
869
  return answer
862
870
 
863
871
 
872
+ #: The last paragraph of every `repo_map` reply, refusal excepted. FIXED AND MANDATORY.
873
+ #:
874
+ #: Roadmap row 10's build gate was "build only after #4 shows discovery tokens dominate",
875
+ #: and #4 REFUTED it: discovery is 0.114 % of real prompt tokens because 97.8 % of the
876
+ #: bill is `cache_read`. The feature ships on an explicit ruling to build it anyway, as a
877
+ #: PRECISION feature. A surface that let a caller believe the map is a token saving would
878
+ #: say the one thing the measurement forbids, so the refutation travels with every answer
879
+ #: rather than living only in a doc nobody reads at call time.
880
+ #:
881
+ #: The second sentence is the budget's unit, for the same reason: `DEFAULT_BUDGET = 4000`
882
+ #: is "1 K tokens" only at the char/4 convention, whose error bar is unmeasured because
883
+ #: measuring it needs the tokenizer the pure-node ruling forbids. Bytes are what is
884
+ #: enforced, so bytes are what the reply says.
885
+ REPO_MAP_TAIL = (
886
+ "This is a precision pass, not a token saving: this feature's build gate was REFUTED "
887
+ "by measurement — discovery is 0.114% of real prompt tokens, because 97.8% of the "
888
+ "bill is cache_read — so a map does not make a session cheaper. What it buys is the "
889
+ "right file found sooner.\n"
890
+ "The budget above is UTF-8 BYTES of listing, not tokens: neither runtime carries a "
891
+ "model tokenizer and this tool will not pretend to one."
892
+ )
893
+
894
+ #: What a listing says when there is nothing to list. An empty string with two blank lines
895
+ #: around it is not an answer, and "0 files" is already on the header line — this names the
896
+ #: reason, which is the same discipline the omission footer is built on.
897
+ REPO_MAP_EMPTY = "(nothing listed: no file under this root scanned into a definition)"
898
+
899
+
900
+ def repo_map_reply(result: repomap.RepoMap) -> str:
901
+ """The `repo_map` tool's prose, byte for byte, from the structured result.
902
+
903
+ Split out of the handler so the two runtimes have ONE shape to reproduce rather than a
904
+ format string embedded in a `case`, and so the `repomap` conformance suite can compare
905
+ the rendered reply without standing up a server. No float is ever rendered here — see
906
+ `repomap`'s trap (8); every number on the header line is an int.
907
+ """
908
+ focus = (
909
+ ", ".join(result.focus)
910
+ if result.focus
911
+ else "(none) — plain centrality over the whole tree"
912
+ )
913
+ head = (
914
+ f"repo map: {result.nodes} files scanned, {result.definitions} definitions, "
915
+ f"{result.edges} edges.\n"
916
+ f"focus: {focus}\n"
917
+ f"budget: {result.budget} UTF-8 bytes; listing {result.listing_bytes} bytes; "
918
+ f"rendered {result.files_rendered} files, "
919
+ f"{result.definitions_rendered} definitions."
920
+ )
921
+ body = result.text if result.text else REPO_MAP_EMPTY
922
+ return f"{head}\n\n{body}\n\n{REPO_MAP_TAIL}"
923
+
924
+
864
925
  def build_server(memory: Memory, log: EventLog | None = None) -> Any:
865
926
  """Assemble the MCP server around one Memory instance (the per-person state).
866
927
 
@@ -984,6 +1045,34 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
984
1045
  )
985
1046
  return _noted(outcome.reply)
986
1047
 
1048
+ def memory_dream(dry_run: bool | None = None) -> str:
1049
+ """Consolidate what the project and the machine-wide profile layer both hold.
1050
+
1051
+ `dry_run` DEFAULTS TO TRUE and the default lives HERE rather than in the
1052
+ component: a client that omits the argument gets `None` through pydantic's
1053
+ `bool | None = None`, and turning that into the safe answer is this handler's
1054
+ job. It is the only tool on this surface that writes into the user's home
1055
+ directory, and the only one whose effect is machine-wide — a fact archived out
1056
+ of the profile store stops answering for every other project on this machine
1057
+ with no store of its own — so the short call is the preview.
1058
+
1059
+ The status is a decision the pass already made (`DreamResult.applied`,
1060
+ `.over_budget`, `.changes`), never a match on the reply.
1061
+ """
1062
+ with _record_raise(log, "memory_dream"):
1063
+ outcome = memory.dream_outcome(True if dry_run is None else bool(dry_run))
1064
+ log.record(
1065
+ "memory_dream",
1066
+ outcome.status,
1067
+ {
1068
+ "absolutised": outcome.absolutised,
1069
+ "consumed": outcome.consumed,
1070
+ "dry_run": outcome.dry_run,
1071
+ "merged": outcome.merged,
1072
+ },
1073
+ )
1074
+ return _noted(outcome.reply)
1075
+
987
1076
  def validate_json(output: str, schema: dict[str, Any]) -> dict[str, Any]:
988
1077
  with _record_raise(log, "validate_json"):
989
1078
  error = schema_error(output, schema)
@@ -1215,13 +1304,89 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
1215
1304
  )
1216
1305
  return _noted(result.as_json())
1217
1306
 
1307
+ def repo_map(
1308
+ root: str,
1309
+ focus: list[str] | None = None,
1310
+ budget: int | None = None,
1311
+ ) -> str:
1312
+ """The ranked definition map on the MCP surface: `repomap` measures, this serves it.
1313
+
1314
+ THE THREE REFUSALS LIVE HERE AND NOT IN `repomap.py`, and that is deliberate.
1315
+ `repo_map()` over a root that does not exist answers an EMPTY map on both runtimes
1316
+ — `os.walk` yields nothing for a missing directory and `walkSources`' `readdirSync`
1317
+ catch does the same — which is the right answer for a library and the wrong one for
1318
+ a tool: a caller who typed the path wrong would be told the tree holds no source.
1319
+ So the argument checks are the SURFACE's, the way `bantamkit_read`'s
1320
+ `refused-offset` is, and the engine J45-9/J45-10 proved byte-identical is not
1321
+ touched by this unit.
1322
+
1323
+ `focus` names the files the caller already has open; they are excluded from the
1324
+ listing. A focus entry that is not a scanned source is IGNORED, not refused —
1325
+ `repo_map` documents that, and refusing would make the tool useless the moment a
1326
+ caller named a file the scanner has no dialect for.
1327
+
1328
+ THE LAST PARAGRAPH IS FIXED AND MANDATORY. Row 10's build gate was refuted by
1329
+ measurement (0.114 % of real prompt tokens) and the feature ships on an explicit
1330
+ ruling to build it anyway; a surface that let a caller believe the map is a saving
1331
+ would be the one sentence this whole feature is not allowed to say.
1332
+
1333
+ THE RECORD IS A DECISION AND HOLDS NO PATH. `root` is what the operator typed and
1334
+ `focus` is the name of the file they are editing; neither is a decision this
1335
+ handler made, so neither is written down. The counts are.
1336
+ """
1337
+ with _record_raise(log, "repo_map"):
1338
+ names = [str(f) for f in (focus or [])]
1339
+ wanted = repomap.DEFAULT_BUDGET if budget is None else int(budget)
1340
+ base = Path(root)
1341
+ if not root:
1342
+ log.record("repo_map", "refused")
1343
+ return _noted(
1344
+ tool_failed(
1345
+ "repo_map", "root must not be empty; name the directory to map"
1346
+ )
1347
+ )
1348
+ if wanted < 0:
1349
+ log.record("repo_map", "refused")
1350
+ return _noted(
1351
+ tool_failed("repo_map", f"budget must not be negative; got {wanted}")
1352
+ )
1353
+ # `Path.exists()` and `Path.is_dir()` both SWALLOW the not-here errno family
1354
+ # (ENOENT, ENOTDIR, ELOOP, EBADF) and re-raise anything else, so a dangling
1355
+ # symlink and `a-file.py/sub` are both "no such directory" while a permission
1356
+ # failure flies to `_record_raise` rather than being dressed up as a missing
1357
+ # tree. Node's `statSync` arm reproduces exactly that split; there is no
1358
+ # `except OSError` here because `repomap.repo_map` raises none — its walk and
1359
+ # its reads each already resolve a failure into a counted Omission.
1360
+ if not base.exists():
1361
+ log.record("repo_map", "refused")
1362
+ return _noted(tool_failed("repo_map", f"no such directory: {root}"))
1363
+ if not base.is_dir():
1364
+ log.record("repo_map", "refused")
1365
+ return _noted(
1366
+ tool_failed("repo_map", f"{root} is a file, not a directory to map")
1367
+ )
1368
+ result = repomap.repo_map(base, focus=names, budget=wanted)
1369
+ log.record(
1370
+ "repo_map",
1371
+ "mapped",
1372
+ {
1373
+ "definitions": result.definitions,
1374
+ "edges": result.edges,
1375
+ "files_rendered": result.files_rendered,
1376
+ "listing_bytes": result.listing_bytes,
1377
+ "nodes": result.nodes,
1378
+ },
1379
+ )
1380
+ return _noted(repo_map_reply(result))
1381
+
1218
1382
  # The served surface, in one place, read out of the asset pack. Adding a tool here
1219
1383
  # without an asset raises AssetNotFound at startup — the manifest cannot drift behind
1220
1384
  # the server, because the server cannot start without it.
1221
1385
  #
1222
1386
  # `bantamkit_status` went LAST rather than first, `memory_compact` after it rather
1223
1387
  # than beside `memory_save` where a reader would look for it, `bantamkit_read`
1224
- # after that and `skill_audit` after that. Registration order IS the served order
1388
+ # after that, `skill_audit` after that, `memory_dream` after that and `repo_map`
1389
+ # after that. Registration order IS the served order
1225
1390
  # (`test_tool_manifest.py::test_the_golden_records_the_order_the_wire_actually_
1226
1391
  # serves`), and appending is the only edit that leaves the others where every
1227
1392
  # existing declaration says they are.
@@ -1237,6 +1402,8 @@ def build_server(memory: Memory, log: EventLog | None = None) -> Any:
1237
1402
  _from_manifest(memory_compact, "memory_compact"),
1238
1403
  _from_manifest(bantamkit_read, "bantamkit_read"),
1239
1404
  _from_manifest(skill_audit, "skill_audit"),
1405
+ _from_manifest(memory_dream, "memory_dream"),
1406
+ _from_manifest(repo_map, "repo_map"),
1240
1407
  ]
1241
1408
 
1242
1409
  server = MCPServer(
@@ -1,4 +1,10 @@
1
- from bantamkit.memory.component import CompactOutcome, Memory, RecallOutcome, SaveOutcome
1
+ from bantamkit.memory.component import (
2
+ CompactOutcome,
3
+ DreamOutcome,
4
+ Memory,
5
+ RecallOutcome,
6
+ SaveOutcome,
7
+ )
2
8
  from bantamkit.memory.divergence import (
3
9
  BodyDiff,
4
10
  DivergenceReport,
@@ -10,6 +16,13 @@ from bantamkit.memory.divergence import (
10
16
  parse_fact,
11
17
  read_store,
12
18
  )
19
+ from bantamkit.memory.dream import (
20
+ DreamMerge,
21
+ DreamResult,
22
+ SimilarPair,
23
+ Superseded,
24
+ dream,
25
+ )
13
26
  from bantamkit.memory.layers import (
14
27
  MEMORY_DIR_ENV,
15
28
  StoreBinding,
@@ -18,6 +31,7 @@ from bantamkit.memory.layers import (
18
31
  )
19
32
  from bantamkit.memory.store import (
20
33
  DEFAULT_INDEX_BUDGET,
34
+ RECALL_MIN_SCORE_RATIO,
21
35
  ArchivedFact,
22
36
  CompactResult,
23
37
  Fact,
@@ -2,7 +2,7 @@
2
2
 
3
3
  `docs/memory.md` states, as a design decision, that `lint`, `archived` and
4
4
  `restore` are **not** agent tools — "lifecycle is an operator decision, not a
5
- model decision" — and the eleven-tool surface holds to it; `compact` is the one
5
+ model decision" — and the thirteen-tool surface holds to it; `compact` is the one
6
6
  exception since job42 (`memory_compact`, the on-refusal path the model reaches
7
7
  itself). That position is only coherent if the operator can actually make the
8
8
  decision. Measured
@@ -10,6 +10,8 @@ from pathlib import Path
10
10
  from bantamkit.agent import Agent, ToolDef
11
11
  from bantamkit.assets import load_skill, load_tool
12
12
  from bantamkit.client import BantamError
13
+ from bantamkit.memory.dream import SUPERSEDED_HEADING, DreamResult
14
+ from bantamkit.memory.dream import dream as _dream
13
15
  from bantamkit.memory.layers import (
14
16
  MEMORY_DIR_ENV,
15
17
  PROJECT_STORE,
@@ -20,6 +22,7 @@ from bantamkit.memory.layers import (
20
22
  )
21
23
  from bantamkit.memory.store import (
22
24
  DEFAULT_INDEX_BUDGET,
25
+ RECALL_MIN_SCORE_RATIO,
23
26
  Fact,
24
27
  MemoryBudgetExceeded,
25
28
  MemoryStore,
@@ -114,6 +117,32 @@ class CompactOutcome:
114
117
  budget: int
115
118
 
116
119
 
120
+ @dataclass(frozen=True)
121
+ class DreamOutcome:
122
+ """What `dream` DID, beside the sentence it says about it.
123
+
124
+ The same seam as `SaveOutcome`, `RecallOutcome` and `CompactOutcome`: `status` is read
125
+ off a decision the pass already made — `DreamResult.applied`, `.over_budget`,
126
+ `.changes` — and never off the reply. `result` carries the whole diff for a caller that
127
+ wants the numbers rather than the prose.
128
+
129
+ `status` is one of `consolidated`, `previewed`, `nothing-to-consolidate`,
130
+ `refused-budget`, `no-profile-layer`.
131
+ """
132
+
133
+ reply: str
134
+ status: str
135
+ dry_run: bool
136
+ merged: int
137
+ consumed: int
138
+ absolutised: int
139
+ superseded: int
140
+ index_before: int
141
+ index_after: int
142
+ budget: int
143
+ result: DreamResult | None = None
144
+
145
+
117
146
  def _profile_store() -> Path:
118
147
  """The last layer `layered` appends, named once so other code can recognise it.
119
148
 
@@ -259,7 +288,12 @@ class Memory:
259
288
  )
260
289
  return SaveOutcome(reply=f"saved '{result.name}'", status="saved")
261
290
 
262
- def recall(self, query: str, k: int | None = None) -> str:
291
+ def recall(
292
+ self,
293
+ query: str,
294
+ k: int | None = None,
295
+ min_ratio: float = RECALL_MIN_SCORE_RATIO,
296
+ ) -> str:
263
297
  """`k` is the model asking for *more*, never for less than the store's default.
264
298
 
265
299
  Measured cause (RB-P1, qwen2.5:14b-instruct): 57 of 60 `memory_recall` calls
@@ -273,9 +307,14 @@ class Memory:
273
307
  The floor is the operator's configured default, not a constant, so a consumer
274
308
  who really wants top-1 says so once at construction (`Memory(store, k=1)`).
275
309
  """
276
- return self.recall_outcome(query, k).reply
310
+ return self.recall_outcome(query, k, min_ratio).reply
277
311
 
278
- def recall_outcome(self, query: str, k: int | None = None) -> RecallOutcome:
312
+ def recall_outcome(
313
+ self,
314
+ query: str,
315
+ k: int | None = None,
316
+ min_ratio: float = RECALL_MIN_SCORE_RATIO,
317
+ ) -> RecallOutcome:
279
318
  """`recall`, carrying the walk it performed as numbers rather than as prose.
280
319
 
281
320
  The reply is byte-for-byte what `recall` has always returned; every field beside
@@ -291,6 +330,17 @@ class Memory:
291
330
  layer whose directory refuses to list contributes nothing and raises the
292
331
  `unreadable` count instead of being scored as empty; that distinction is the
293
332
  whole subject of `_nothing_to_report` below and must not be undone here.
333
+
334
+ `min_ratio` (roadmap #6) rides through to every layer's `MemoryStore.recall`
335
+ unchanged, so the gate is measured against EACH LAYER's own best score and never
336
+ across layers: a profile fact does not have to out-score the project store's top
337
+ hit to be admitted, because the two stores are answering as two stores. Its
338
+ default is `RECALL_MIN_SCORE_RATIO` = 0.0, which keeps every fact `recall` was
339
+ going to return, and nothing on the tool path passes anything else today. A ratio
340
+ outside `[0.0, 1.0]` raises out of the FIRST layer, which is the writable project
341
+ store, so it surfaces as the error it is rather than as an `unreadable` count —
342
+ the read-only-layer `except` below would otherwise file a caller's bad argument as
343
+ a corrupt grant.
294
344
  """
295
345
  budget = self.k if k is None else max(k, self.k)
296
346
  picked: list[tuple[str, Fact]] = []
@@ -303,7 +353,7 @@ class Memory:
303
353
  break # budget spent: later (read-only) layers are never even read
304
354
  reached += 1
305
355
  try:
306
- facts = store.recall(query, budget, stamp=writable)
356
+ facts = store.recall(query, budget, stamp=writable, min_ratio=min_ratio)
307
357
  except (BantamError, OSError, UnicodeDecodeError):
308
358
  if writable:
309
359
  raise # the project layer failing is a real error, as in v1
@@ -560,6 +610,158 @@ class Memory:
560
610
  budget=result.budget,
561
611
  )
562
612
 
613
+ def dream(self, dry_run: bool = True) -> str:
614
+ """Consolidate what the project and profile layers hold under the same name."""
615
+ return self.dream_outcome(dry_run).reply
616
+
617
+ def dream_outcome(self, dry_run: bool = True) -> DreamOutcome:
618
+ """`dream`, with the decision it took carried beside the sentence it wrote.
619
+
620
+ ONLY THE PROFILE LAYER IS CONSUMED. `_layers` also carries read-only GRANTS, and a
621
+ grant is another operator's store: consolidating a fact out of one is not this
622
+ person's move to make, so `dream` never looks at them. The label is matched exactly
623
+ (`profile`), never by prefix, because a grant is labelled `extra:<name>` and a
624
+ prefix match on a directory called `profile-something` would reach one.
625
+
626
+ A `Memory` constructed directly — not through `layered` — has no profile layer at
627
+ all, and that is `no-profile-layer` rather than an error: there is nothing to
628
+ consolidate ACROSS when only one layer is bound.
629
+
630
+ `dry_run` DEFAULTS TO TRUE. This is the only op in this component that writes into
631
+ the user's home directory, and it is the only one whose effect is machine-wide: a
632
+ fact archived out of the profile store stops answering for every other project on
633
+ this machine that has no store of its own. A destructive consolidation nobody can
634
+ preview is not shippable, so the safe call is the short one.
635
+ """
636
+ profile = next((store for label, store, _ in self._layers if label == "profile"), None)
637
+ if profile is None:
638
+ return DreamOutcome(
639
+ reply=(
640
+ "nothing to consolidate: no profile layer is bound, so the project "
641
+ f"store {self.store.root} is the only layer there is."
642
+ ),
643
+ status="no-profile-layer",
644
+ dry_run=dry_run,
645
+ merged=0,
646
+ consumed=0,
647
+ absolutised=0,
648
+ superseded=0,
649
+ index_before=0,
650
+ index_after=0,
651
+ budget=self.store.index_budget,
652
+ )
653
+ result = _dream(self.store, profile, dry_run)
654
+ if result.over_budget:
655
+ status = "refused-budget"
656
+ elif not result.changes:
657
+ status = "nothing-to-consolidate"
658
+ elif result.applied:
659
+ status = "consolidated"
660
+ else:
661
+ status = "previewed"
662
+ return DreamOutcome(
663
+ reply=self._dream_reply(status, result),
664
+ status=status,
665
+ dry_run=result.dry_run,
666
+ merged=len(result.merged),
667
+ consumed=len(result.consumed),
668
+ absolutised=len(result.absolutised),
669
+ superseded=len(result.superseded),
670
+ index_before=result.index_before,
671
+ index_after=result.index_after,
672
+ budget=result.budget,
673
+ result=result,
674
+ )
675
+
676
+ @staticmethod
677
+ def _dream_reply(status: str, result: DreamResult) -> str:
678
+ """The whole diff as prose: what merged, what was dated, what moved, what it cost.
679
+
680
+ THE HONESTY SENTENCE AT THE END IS NOT DECORATION. Row 5 was planned as a token
681
+ saving and J45-1 measured that it is not one: the profile store has no `index.md`
682
+ on disk, its index is derived at read time, and nothing loads it into a prompt — so
683
+ deduplicating it frees approximately zero prompt bytes. The value is that one
684
+ ruling now has one copy instead of two that can disagree, and the reply says which
685
+ of those two things the operator just bought.
686
+ """
687
+ lines: list[str] = []
688
+ if status == "nothing-to-consolidate":
689
+ lines.append(
690
+ f"nothing to consolidate: {result.project_root} and {result.profile_root} "
691
+ f"hold no fact under the same name, and no project fact carries a relative "
692
+ f"date this pass will resolve."
693
+ )
694
+ else:
695
+ identical = sum(1 for merge in result.merged if merge.kind == "identical")
696
+ diverged = len(result.merged) - identical
697
+ verb = {
698
+ "consolidated": "consolidated",
699
+ "previewed": "would consolidate",
700
+ "refused-budget": "would consolidate",
701
+ }[status]
702
+ lines.append(
703
+ f"{verb} {len(result.merged)} cross-layer name collision(s) — {identical} "
704
+ f"byte-identical, {diverged} diverged and unioned — and rewrote "
705
+ f"{len(result.rewritten)} project fact(s). The survivor stays in the "
706
+ f"project layer; the profile copy moves to {result.archive_dir} and is NOT "
707
+ f"deleted: restore it by name."
708
+ )
709
+ for merge in result.merged:
710
+ lines.append(
711
+ f"- {merge.name} ({merge.kind}, name+description similarity "
712
+ f"{merge.jaccard:.3f}) — body {merge.body_before} -> {merge.body_after} "
713
+ f"bytes, {merge.blocks_added} block(s) kept from the {merge.consumed_layer} "
714
+ f"copy, survivor in {merge.survivor_layer}"
715
+ )
716
+ for record in result.superseded:
717
+ lines.append(
718
+ f"- superseded '{record.subject}': the {record.lost_layer} copy "
719
+ f"({record.lost_date}) lost to the {record.kept_layer} copy "
720
+ f"({record.kept_date}); the older claim is kept verbatim under "
721
+ f"'{SUPERSEDED_HEADING}'"
722
+ )
723
+ for hit in result.absolutised:
724
+ lines.append(
725
+ f"- dated {hit.name} ({hit.layer}): '{hit.term}' -> {hit.resolved}, "
726
+ f"resolved against that fact's own mtime {hit.basis}, not today"
727
+ )
728
+ for hit in result.unresolved:
729
+ lines.append(
730
+ f"- left alone in {hit.name} ({hit.layer}): '{hit.term}' — no exact day "
731
+ f"follows from an mtime, so nothing was substituted"
732
+ )
733
+ for name, reason in result.refused:
734
+ lines.append(f"- refused {name}: {reason}")
735
+ for pair in result.similar_unmerged:
736
+ lines.append(
737
+ f"- similar but NOT merged: {pair.project_name} (project) and "
738
+ f"{pair.profile_name} (profile) score {pair.jaccard:.3f}; this pass merges "
739
+ f"on name equality only, so nothing was done about it"
740
+ )
741
+ lines.append(
742
+ f"project index {result.index_before} -> {result.index_after} bytes against a "
743
+ f"{result.budget}-byte budget; the profile index (derived, no file on disk) "
744
+ f"{result.profile_index_before} -> {result.profile_index_after}; fact bytes "
745
+ f"across both layers {result.fact_bytes_before} -> {result.fact_bytes_after}."
746
+ )
747
+ if status == "refused-budget":
748
+ lines.append(
749
+ f"NOTHING WAS WRITTEN: the merged index would be {result.index_after} bytes "
750
+ f"against a {result.budget}-byte budget. Call `memory_compact` to archive "
751
+ f"the stalest facts, then run this again."
752
+ )
753
+ elif result.dry_run:
754
+ lines.append(
755
+ "DRY RUN: nothing was written. Re-run with dry_run false to apply it."
756
+ )
757
+ lines.append(
758
+ "This is a correctness pass, not a token saving: the profile index is derived "
759
+ "at read time and is not loaded from a file, so consolidating it frees "
760
+ "approximately no prompt bytes. What it buys is one copy of a ruling instead "
761
+ "of two that can diverge."
762
+ )
763
+ return "\n".join(lines)
764
+
563
765
  # Back-compat aliases: the component's API predates the public names.
564
766
  _save = save
565
767
  _recall = recall