thth 2.3.2__tar.gz → 2.3.3__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 (255) hide show
  1. {thth-2.3.2 → thth-2.3.3}/PKG-INFO +2 -2
  2. {thth-2.3.2 → thth-2.3.3}/README.md +1 -1
  3. {thth-2.3.2 → thth-2.3.3}/skills/thth/SKILL.md +7 -3
  4. {thth-2.3.2 → thth-2.3.3}/tests/test_behind_remote.py +5 -0
  5. thth-2.3.3/tests/test_collect_engagements.py +175 -0
  6. thth-2.3.3/tests/test_read_cli_bare_runtimeerror.py +179 -0
  7. {thth-2.3.2 → thth-2.3.3}/tests/test_skill_doc.py +26 -0
  8. thth-2.3.3/thth/VERSION +1 -0
  9. {thth-2.3.2 → thth-2.3.3}/thth/collect.py +56 -2
  10. {thth-2.3.2 → thth-2.3.3}/thth/thread_read.py +16 -0
  11. {thth-2.3.2 → thth-2.3.3}/thth/threads_read_cli.py +13 -0
  12. {thth-2.3.2 → thth-2.3.3}/thth/where_cli.py +10 -0
  13. {thth-2.3.2 → thth-2.3.3}/thth/who_cli.py +8 -0
  14. thth-2.3.2/thth/VERSION +0 -1
  15. {thth-2.3.2 → thth-2.3.3}/.gitignore +0 -0
  16. {thth-2.3.2 → thth-2.3.3}/LICENSE +0 -0
  17. {thth-2.3.2 → thth-2.3.3}/accounts.example/bluesky.json +0 -0
  18. {thth-2.3.2 → thth-2.3.3}/accounts.example/mastodon.json +0 -0
  19. {thth-2.3.2 → thth-2.3.3}/accounts.example/threads.json +0 -0
  20. {thth-2.3.2 → thth-2.3.3}/bin/thth +0 -0
  21. {thth-2.3.2 → thth-2.3.3}/bin/thth-remote +0 -0
  22. {thth-2.3.2 → thth-2.3.3}/bin/thth-run +0 -0
  23. {thth-2.3.2 → thth-2.3.3}/mcp/server.py +0 -0
  24. {thth-2.3.2 → thth-2.3.3}/pyproject.toml +0 -0
  25. {thth-2.3.2 → thth-2.3.3}/systemd/thth@.service +0 -0
  26. {thth-2.3.2 → thth-2.3.3}/tests/__init__.py +0 -0
  27. {thth-2.3.2 → thth-2.3.3}/tests/conftest.py +0 -0
  28. {thth-2.3.2 → thth-2.3.3}/tests/fixtures/build_injection_eval.py +0 -0
  29. {thth-2.3.2 → thth-2.3.3}/tests/fixtures/callback/worker_responses.json +0 -0
  30. {thth-2.3.2 → thth-2.3.3}/tests/fixtures/injection_eval.json +0 -0
  31. {thth-2.3.2 → thth-2.3.3}/tests/fixtures/umami-bile.md +0 -0
  32. {thth-2.3.2 → thth-2.3.3}/tests/helpers/__init__.py +0 -0
  33. {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_oauth_server.py +0 -0
  34. {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_push_adapter.py +0 -0
  35. {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_redirect_server.py +0 -0
  36. {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_search_adapter.py +0 -0
  37. {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_threads_reader.py +0 -0
  38. {thth-2.3.2 → thth-2.3.3}/tests/helpers/fake_threads_writes.py +0 -0
  39. {thth-2.3.2 → thth-2.3.3}/tests/helpers/hold_lock.py +0 -0
  40. {thth-2.3.2 → thth-2.3.3}/tests/helpers/hold_repo_lock.py +0 -0
  41. {thth-2.3.2 → thth-2.3.3}/tests/helpers/worker_probe.mjs +0 -0
  42. {thth-2.3.2 → thth-2.3.3}/tests/test_account_name_check.py +0 -0
  43. {thth-2.3.2 → thth-2.3.3}/tests/test_account_report.py +0 -0
  44. {thth-2.3.2 → thth-2.3.3}/tests/test_accounts_dir.py +0 -0
  45. {thth-2.3.2 → thth-2.3.3}/tests/test_adapter_boundary.py +0 -0
  46. {thth-2.3.2 → thth-2.3.3}/tests/test_after_cli.py +0 -0
  47. {thth-2.3.2 → thth-2.3.3}/tests/test_api_error_body.py +0 -0
  48. {thth-2.3.2 → thth-2.3.3}/tests/test_app_env_cli.py +0 -0
  49. {thth-2.3.2 → thth-2.3.3}/tests/test_approval.py +0 -0
  50. {thth-2.3.2 → thth-2.3.3}/tests/test_approve_cmd.py +0 -0
  51. {thth-2.3.2 → thth-2.3.3}/tests/test_ask.py +0 -0
  52. {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_fifth_review.py +0 -0
  53. {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_fourth_review.py +0 -0
  54. {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_review.py +0 -0
  55. {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_second_review.py +0 -0
  56. {thth-2.3.2 → thth-2.3.3}/tests/test_atlas_third_review.py +0 -0
  57. {thth-2.3.2 → thth-2.3.3}/tests/test_audit1_fixes.py +0 -0
  58. {thth-2.3.2 → thth-2.3.3}/tests/test_audit2_fixes.py +0 -0
  59. {thth-2.3.2 → thth-2.3.3}/tests/test_audit_b_counterexamples.py +0 -0
  60. {thth-2.3.2 → thth-2.3.3}/tests/test_audit_b_m1_semantics.py +0 -0
  61. {thth-2.3.2 → thth-2.3.3}/tests/test_audit_b_n1_depth.py +0 -0
  62. {thth-2.3.2 → thth-2.3.3}/tests/test_audit_b_r1_closure.py +0 -0
  63. {thth-2.3.2 → thth-2.3.3}/tests/test_audit_v201.py +0 -0
  64. {thth-2.3.2 → thth-2.3.3}/tests/test_audit_v23_details.py +0 -0
  65. {thth-2.3.2 → thth-2.3.3}/tests/test_auth.py +0 -0
  66. {thth-2.3.2 → thth-2.3.3}/tests/test_auth_identity_required.py +0 -0
  67. {thth-2.3.2 → thth-2.3.3}/tests/test_bluesky_adapter.py +0 -0
  68. {thth-2.3.2 → thth-2.3.3}/tests/test_bluesky_secret_scrub.py +0 -0
  69. {thth-2.3.2 → thth-2.3.3}/tests/test_board_needs_review.py +0 -0
  70. {thth-2.3.2 → thth-2.3.3}/tests/test_board_running.py +0 -0
  71. {thth-2.3.2 → thth-2.3.3}/tests/test_bundle.py +0 -0
  72. {thth-2.3.2 → thth-2.3.3}/tests/test_charcount.py +0 -0
  73. {thth-2.3.2 → thth-2.3.3}/tests/test_cli_entrypoint.py +0 -0
  74. {thth-2.3.2 → thth-2.3.3}/tests/test_cli_help.py +0 -0
  75. {thth-2.3.2 → thth-2.3.3}/tests/test_cli_vm_path.py +0 -0
  76. {thth-2.3.2 → thth-2.3.3}/tests/test_collect.py +0 -0
  77. {thth-2.3.2 → thth-2.3.3}/tests/test_collect_runs.py +0 -0
  78. {thth-2.3.2 → thth-2.3.3}/tests/test_collect_sent.py +0 -0
  79. {thth-2.3.2 → thth-2.3.3}/tests/test_comparability.py +0 -0
  80. {thth-2.3.2 → thth-2.3.3}/tests/test_control_chars_refused.py +0 -0
  81. {thth-2.3.2 → thth-2.3.3}/tests/test_conversation_paging.py +0 -0
  82. {thth-2.3.2 → thth-2.3.3}/tests/test_count_for.py +0 -0
  83. {thth-2.3.2 → thth-2.3.3}/tests/test_docs_links.py +0 -0
  84. {thth-2.3.2 → thth-2.3.3}/tests/test_doctor.py +0 -0
  85. {thth-2.3.2 → thth-2.3.3}/tests/test_doctor_all_permissions.py +0 -0
  86. {thth-2.3.2 → thth-2.3.3}/tests/test_doctor_next_step.py +0 -0
  87. {thth-2.3.2 → thth-2.3.3}/tests/test_duplicate_front_matter_keys.py +0 -0
  88. {thth-2.3.2 → thth-2.3.3}/tests/test_editorial_audit.py +0 -0
  89. {thth-2.3.2 → thth-2.3.3}/tests/test_editorial_n3_n4.py +0 -0
  90. {thth-2.3.2 → thth-2.3.3}/tests/test_engagements.py +0 -0
  91. {thth-2.3.2 → thth-2.3.3}/tests/test_external_acceptance.py +0 -0
  92. {thth-2.3.2 → thth-2.3.3}/tests/test_fake_api.py +0 -0
  93. {thth-2.3.2 → thth-2.3.3}/tests/test_form_spec.py +0 -0
  94. {thth-2.3.2 → thth-2.3.3}/tests/test_form_spec_cli.py +0 -0
  95. {thth-2.3.2 → thth-2.3.3}/tests/test_forms.py +0 -0
  96. {thth-2.3.2 → thth-2.3.3}/tests/test_fresh_install.py +0 -0
  97. {thth-2.3.2 → thth-2.3.3}/tests/test_front_matter_injection.py +0 -0
  98. {thth-2.3.2 → thth-2.3.3}/tests/test_hypotheses.py +0 -0
  99. {thth-2.3.2 → thth-2.3.3}/tests/test_hypothesis_boundaries.py +0 -0
  100. {thth-2.3.2 → thth-2.3.3}/tests/test_hypothesis_units.py +0 -0
  101. {thth-2.3.2 → thth-2.3.3}/tests/test_impact_cli.py +0 -0
  102. {thth-2.3.2 → thth-2.3.3}/tests/test_inbox_granted_scopes.py +0 -0
  103. {thth-2.3.2 → thth-2.3.3}/tests/test_independent_thread_review.py +0 -0
  104. {thth-2.3.2 → thth-2.3.3}/tests/test_independent_topic_review.py +0 -0
  105. {thth-2.3.2 → thth-2.3.3}/tests/test_independent_topic_review_r2.py +0 -0
  106. {thth-2.3.2 → thth-2.3.3}/tests/test_inflight.py +0 -0
  107. {thth-2.3.2 → thth-2.3.3}/tests/test_jst.py +0 -0
  108. {thth-2.3.2 → thth-2.3.3}/tests/test_lint.py +0 -0
  109. {thth-2.3.2 → thth-2.3.3}/tests/test_list_projections.py +0 -0
  110. {thth-2.3.2 → thth-2.3.3}/tests/test_llm_trial.py +0 -0
  111. {thth-2.3.2 → thth-2.3.3}/tests/test_llm_trial_engage.py +0 -0
  112. {thth-2.3.2 → thth-2.3.3}/tests/test_lock.py +0 -0
  113. {thth-2.3.2 → thth-2.3.3}/tests/test_m2_independent.py +0 -0
  114. {thth-2.3.2 → thth-2.3.3}/tests/test_maintain.py +0 -0
  115. {thth-2.3.2 → thth-2.3.3}/tests/test_mastodon_adapter.py +0 -0
  116. {thth-2.3.2 → thth-2.3.3}/tests/test_mastodon_visibility.py +0 -0
  117. {thth-2.3.2 → thth-2.3.3}/tests/test_max_per_run.py +0 -0
  118. {thth-2.3.2 → thth-2.3.3}/tests/test_mcp.py +0 -0
  119. {thth-2.3.2 → thth-2.3.3}/tests/test_mcp_robustness.py +0 -0
  120. {thth-2.3.2 → thth-2.3.3}/tests/test_measured.py +0 -0
  121. {thth-2.3.2 → thth-2.3.3}/tests/test_media_accounts.py +0 -0
  122. {thth-2.3.2 → thth-2.3.3}/tests/test_media_next_steps.py +0 -0
  123. {thth-2.3.2 → thth-2.3.3}/tests/test_media_roundtrip.py +0 -0
  124. {thth-2.3.2 → thth-2.3.3}/tests/test_mismatch_diagnostics.py +0 -0
  125. {thth-2.3.2 → thth-2.3.3}/tests/test_n5_reply_null_element.py +0 -0
  126. {thth-2.3.2 → thth-2.3.3}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  127. {thth-2.3.2 → thth-2.3.3}/tests/test_n7_doctor_error_body.py +0 -0
  128. {thth-2.3.2 → thth-2.3.3}/tests/test_no_limits.py +0 -0
  129. {thth-2.3.2 → thth-2.3.3}/tests/test_observation_provenance.py +0 -0
  130. {thth-2.3.2 → thth-2.3.3}/tests/test_packaging.py +0 -0
  131. {thth-2.3.2 → thth-2.3.3}/tests/test_preview.py +0 -0
  132. {thth-2.3.2 → thth-2.3.3}/tests/test_production_mode.py +0 -0
  133. {thth-2.3.2 → thth-2.3.3}/tests/test_pull_cli.py +0 -0
  134. {thth-2.3.2 → thth-2.3.3}/tests/test_queue.py +0 -0
  135. {thth-2.3.2 → thth-2.3.3}/tests/test_quiet_hours.py +0 -0
  136. {thth-2.3.2 → thth-2.3.3}/tests/test_r2_r4_r5.py +0 -0
  137. {thth-2.3.2 → thth-2.3.3}/tests/test_reason_adoption.py +0 -0
  138. {thth-2.3.2 → thth-2.3.3}/tests/test_redact.py +0 -0
  139. {thth-2.3.2 → thth-2.3.3}/tests/test_redirect_token_leak.py +0 -0
  140. {thth-2.3.2 → thth-2.3.3}/tests/test_refresh.py +0 -0
  141. {thth-2.3.2 → thth-2.3.3}/tests/test_refresh_destination.py +0 -0
  142. {thth-2.3.2 → thth-2.3.3}/tests/test_rejudge_new.py +0 -0
  143. {thth-2.3.2 → thth-2.3.3}/tests/test_rejudge_r2_r3.py +0 -0
  144. {thth-2.3.2 → thth-2.3.3}/tests/test_replies.py +0 -0
  145. {thth-2.3.2 → thth-2.3.3}/tests/test_replies_refresh.py +0 -0
  146. {thth-2.3.2 → thth-2.3.3}/tests/test_repo_lock.py +0 -0
  147. {thth-2.3.2 → thth-2.3.3}/tests/test_repo_sync.py +0 -0
  148. {thth-2.3.2 → thth-2.3.3}/tests/test_reverse_audit.py +0 -0
  149. {thth-2.3.2 → thth-2.3.3}/tests/test_review_case_id_cluster_m2.py +0 -0
  150. {thth-2.3.2 → thth-2.3.3}/tests/test_review_case_id_cluster_r2.py +0 -0
  151. {thth-2.3.2 → thth-2.3.3}/tests/test_review_cli.py +0 -0
  152. {thth-2.3.2 → thth-2.3.3}/tests/test_review_evidence_account.py +0 -0
  153. {thth-2.3.2 → thth-2.3.3}/tests/test_review_form_spec_and_case_id.py +0 -0
  154. {thth-2.3.2 → thth-2.3.3}/tests/test_review_provenance.py +0 -0
  155. {thth-2.3.2 → thth-2.3.3}/tests/test_review_records.py +0 -0
  156. {thth-2.3.2 → thth-2.3.3}/tests/test_revoke.py +0 -0
  157. {thth-2.3.2 → thth-2.3.3}/tests/test_roundtrip.py +0 -0
  158. {thth-2.3.2 → thth-2.3.3}/tests/test_run_missing_env.py +0 -0
  159. {thth-2.3.2 → thth-2.3.3}/tests/test_run_record_failclosed.py +0 -0
  160. {thth-2.3.2 → thth-2.3.3}/tests/test_runs_collected.py +0 -0
  161. {thth-2.3.2 → thth-2.3.3}/tests/test_runs_read_rows.py +0 -0
  162. {thth-2.3.2 → thth-2.3.3}/tests/test_schedule_multiple.py +0 -0
  163. {thth-2.3.2 → thth-2.3.3}/tests/test_schedule_view.py +0 -0
  164. {thth-2.3.2 → thth-2.3.3}/tests/test_scopes_source_requested.py +0 -0
  165. {thth-2.3.2 → thth-2.3.3}/tests/test_search_engage_targets.py +0 -0
  166. {thth-2.3.2 → thth-2.3.3}/tests/test_secret_value_redact.py +0 -0
  167. {thth-2.3.2 → thth-2.3.3}/tests/test_secrets_grep.py +0 -0
  168. {thth-2.3.2 → thth-2.3.3}/tests/test_security_small_fixes.py +0 -0
  169. {thth-2.3.2 → thth-2.3.3}/tests/test_select.py +0 -0
  170. {thth-2.3.2 → thth-2.3.3}/tests/test_selfupdate.py +0 -0
  171. {thth-2.3.2 → thth-2.3.3}/tests/test_send.py +0 -0
  172. {thth-2.3.2 → thth-2.3.3}/tests/test_send_cli.py +0 -0
  173. {thth-2.3.2 → thth-2.3.3}/tests/test_send_media.py +0 -0
  174. {thth-2.3.2 → thth-2.3.3}/tests/test_send_visibility.py +0 -0
  175. {thth-2.3.2 → thth-2.3.3}/tests/test_sent_integrity.py +0 -0
  176. {thth-2.3.2 → thth-2.3.3}/tests/test_server_json.py +0 -0
  177. {thth-2.3.2 → thth-2.3.3}/tests/test_site.py +0 -0
  178. {thth-2.3.2 → thth-2.3.3}/tests/test_systemd.py +0 -0
  179. {thth-2.3.2 → thth-2.3.3}/tests/test_thread_publish.py +0 -0
  180. {thth-2.3.2 → thth-2.3.3}/tests/test_thread_read.py +0 -0
  181. {thth-2.3.2 → thth-2.3.3}/tests/test_threadrun.py +0 -0
  182. {thth-2.3.2 → thth-2.3.3}/tests/test_threads_read_permissions.py +0 -0
  183. {thth-2.3.2 → thth-2.3.3}/tests/test_threadshape.py +0 -0
  184. {thth-2.3.2 → thth-2.3.3}/tests/test_thth_root.py +0 -0
  185. {thth-2.3.2 → thth-2.3.3}/tests/test_token_exists.py +0 -0
  186. {thth-2.3.2 → thth-2.3.3}/tests/test_token_scopes_record.py +0 -0
  187. {thth-2.3.2 → thth-2.3.3}/tests/test_token_set.py +0 -0
  188. {thth-2.3.2 → thth-2.3.3}/tests/test_topic.py +0 -0
  189. {thth-2.3.2 → thth-2.3.3}/tests/test_topic_advice.py +0 -0
  190. {thth-2.3.2 → thth-2.3.3}/tests/test_topic_cli.py +0 -0
  191. {thth-2.3.2 → thth-2.3.3}/tests/test_topic_models.py +0 -0
  192. {thth-2.3.2 → thth-2.3.3}/tests/test_topic_review_fixes.py +0 -0
  193. {thth-2.3.2 → thth-2.3.3}/tests/test_topic_shelf_observers.py +0 -0
  194. {thth-2.3.2 → thth-2.3.3}/tests/test_topic_store.py +0 -0
  195. {thth-2.3.2 → thth-2.3.3}/tests/test_topics_record_concurrent.py +0 -0
  196. {thth-2.3.2 → thth-2.3.3}/tests/test_topics_tool.py +0 -0
  197. {thth-2.3.2 → thth-2.3.3}/tests/test_trial_frictions.py +0 -0
  198. {thth-2.3.2 → thth-2.3.3}/tests/test_v21b_approved_writes.py +0 -0
  199. {thth-2.3.2 → thth-2.3.3}/tests/test_value_domains_shown.py +0 -0
  200. {thth-2.3.2 → thth-2.3.3}/tests/test_version.py +0 -0
  201. {thth-2.3.2 → thth-2.3.3}/tests/test_where_cli.py +0 -0
  202. {thth-2.3.2 → thth-2.3.3}/tests/test_who_cli.py +0 -0
  203. {thth-2.3.2 → thth-2.3.3}/tests/test_writeback_rebase.py +0 -0
  204. {thth-2.3.2 → thth-2.3.3}/tests/test_writeback_validate.py +0 -0
  205. {thth-2.3.2 → thth-2.3.3}/thth/__init__.py +0 -0
  206. {thth-2.3.2 → thth-2.3.3}/thth/__main__.py +0 -0
  207. {thth-2.3.2 → thth-2.3.3}/thth/account_cli.py +0 -0
  208. {thth-2.3.2 → thth-2.3.3}/thth/account_report.py +0 -0
  209. {thth-2.3.2 → thth-2.3.3}/thth/accounts.py +0 -0
  210. {thth-2.3.2 → thth-2.3.3}/thth/adapters/__init__.py +0 -0
  211. {thth-2.3.2 → thth-2.3.3}/thth/adapters/base.py +0 -0
  212. {thth-2.3.2 → thth-2.3.3}/thth/adapters/bluesky.py +0 -0
  213. {thth-2.3.2 → thth-2.3.3}/thth/adapters/mastodon.py +0 -0
  214. {thth-2.3.2 → thth-2.3.3}/thth/adapters/threads.py +0 -0
  215. {thth-2.3.2 → thth-2.3.3}/thth/after_cli.py +0 -0
  216. {thth-2.3.2 → thth-2.3.3}/thth/appenv.py +0 -0
  217. {thth-2.3.2 → thth-2.3.3}/thth/approval.py +0 -0
  218. {thth-2.3.2 → thth-2.3.3}/thth/ask.py +0 -0
  219. {thth-2.3.2 → thth-2.3.3}/thth/ask_cli.py +0 -0
  220. {thth-2.3.2 → thth-2.3.3}/thth/bundle.py +0 -0
  221. {thth-2.3.2 → thth-2.3.3}/thth/cli.py +0 -0
  222. {thth-2.3.2 → thth-2.3.3}/thth/core.py +0 -0
  223. {thth-2.3.2 → thth-2.3.3}/thth/doctor.py +0 -0
  224. {thth-2.3.2 → thth-2.3.3}/thth/engagements.py +0 -0
  225. {thth-2.3.2 → thth-2.3.3}/thth/forms.py +0 -0
  226. {thth-2.3.2 → thth-2.3.3}/thth/httpsafe.py +0 -0
  227. {thth-2.3.2 → thth-2.3.3}/thth/inflight.py +0 -0
  228. {thth-2.3.2 → thth-2.3.3}/thth/jst.py +0 -0
  229. {thth-2.3.2 → thth-2.3.3}/thth/lint.py +0 -0
  230. {thth-2.3.2 → thth-2.3.3}/thth/lock.py +0 -0
  231. {thth-2.3.2 → thth-2.3.3}/thth/maintain.py +0 -0
  232. {thth-2.3.2 → thth-2.3.3}/thth/measured.py +0 -0
  233. {thth-2.3.2 → thth-2.3.3}/thth/oauth.py +0 -0
  234. {thth-2.3.2 → thth-2.3.3}/thth/postid.py +0 -0
  235. {thth-2.3.2 → thth-2.3.3}/thth/queuefile.py +0 -0
  236. {thth-2.3.2 → thth-2.3.3}/thth/redact.py +0 -0
  237. {thth-2.3.2 → thth-2.3.3}/thth/replies.py +0 -0
  238. {thth-2.3.2 → thth-2.3.3}/thth/report.py +0 -0
  239. {thth-2.3.2 → thth-2.3.3}/thth/retract_cli.py +0 -0
  240. {thth-2.3.2 → thth-2.3.3}/thth/runs.py +0 -0
  241. {thth-2.3.2 → thth-2.3.3}/thth/scopes.py +0 -0
  242. {thth-2.3.2 → thth-2.3.3}/thth/secrets_fs.py +0 -0
  243. {thth-2.3.2 → thth-2.3.3}/thth/select.py +0 -0
  244. {thth-2.3.2 → thth-2.3.3}/thth/selfupdate.py +0 -0
  245. {thth-2.3.2 → thth-2.3.3}/thth/sent.py +0 -0
  246. {thth-2.3.2 → thth-2.3.3}/thth/systemd_gen.py +0 -0
  247. {thth-2.3.2 → thth-2.3.3}/thth/threadrun.py +0 -0
  248. {thth-2.3.2 → thth-2.3.3}/thth/threadshape.py +0 -0
  249. {thth-2.3.2 → thth-2.3.3}/thth/threadthrow.py +0 -0
  250. {thth-2.3.2 → thth-2.3.3}/thth/topic_advice.py +0 -0
  251. {thth-2.3.2 → thth-2.3.3}/thth/topic_cli.py +0 -0
  252. {thth-2.3.2 → thth-2.3.3}/thth/topic_models.py +0 -0
  253. {thth-2.3.2 → thth-2.3.3}/thth/topic_store.py +0 -0
  254. {thth-2.3.2 → thth-2.3.3}/thth/topics.py +0 -0
  255. {thth-2.3.2 → thth-2.3.3}/thth/writeback.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 2.3.2
3
+ Version: 2.3.3
4
4
  Summary: 承認済みの投稿だけを投げる門(Threads・Bluesky・Mastodon)。人が承認し、記録は利用者の git に残る。
5
5
  Project-URL: Homepage, https://thth.me
6
6
  Project-URL: Source, https://github.com/aokings/thth
@@ -45,7 +45,7 @@ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watc
45
45
 
46
46
  ## いまの状態(2026-09-13)
47
47
 
48
- 版 **2.3.2**。全件テスト **2310 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
48
+ 版 **2.3.3**。全件テスト **2320 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
49
49
 
50
50
  **媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
51
51
 
@@ -21,7 +21,7 @@ watchtower(`~/Developer/watchtower`)の隣に同じ流儀で並べる。watc
21
21
 
22
22
  ## いまの状態(2026-09-13)
23
23
 
24
- 版 **2.3.2**。全件テスト **2310 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
24
+ 版 **2.3.3**。全件テスト **2320 件**(`python -m pytest tests/ -q -n auto -p no:cacheprovider`・1 skip・rc=0)。
25
25
 
26
26
  **媒体**: Threads(稼働)・Bluesky・Mastodon(同席用の台帳あり・未稼働)。
27
27
 
@@ -12,7 +12,8 @@ description: 投稿する前・返信を書く前・出したあと・絡みに
12
12
  - **`--production` を付けるまで出ない。** 付けない実行は、投げるはずの本文と digest を
13
13
  表示して終わる。
14
14
  - **記録は利用者の git に残る。** post_id・承認者・承認時刻・返信・実測は、あなたの repo の
15
- commit として残る。THTH のサーバには何も無い。
15
+ commit として残る。THTH のサーバには何も無い。絡みの台帳(`data/sns/engagements/`)は
16
+ 公開の commit とは別で、**次の採取(`thth collect`・最大 10 分の遅れ)**で commit される。
16
17
  - **数値には分母が付く。** 件数・期間・揃えた条件が無い数字は返さない。取れていない刻みは
17
18
  `null` であって `0` ではない。
18
19
  - **黙って間違えない。** 拒むときは理由と次の一手を言って非ゼロで終わる(loud reject)。
@@ -30,8 +31,11 @@ description: 投稿する前・返信を書く前・出したあと・絡みに
30
31
  - **何を書くかを決めない。** 文体・頻度・時刻・語の選び方はあなたのプロジェクトのもの。
31
32
  渡された本文を**整形も切り詰めもせず**そのまま出す。
32
33
  - **読む口は何も保存しない。** `thread_read`・`where_to_appear`・`who_is_this` はどれも、
33
- 読んで見せるだけで**台帳に 1 バイトも書かない**。残るのは**自分の行為と反応**
34
- (絡みの台帳)だけで、**相手の本文・相手の名前・あなたの判断は台帳に入らない**。
34
+ 読んで見せるだけで**台帳に 1 バイトも書かない**。この 3 つと絡みの台帳
35
+ (`data/sns/engagements/`)だけを見れば、残るのは**自分の行為と反応**だけで、
36
+ **相手の本文・相手の名前・あなたの判断は入らない**。**別の台帳**——自分の投稿への
37
+ 返信の台帳(`thth collect` が採り、`thth replies` で読む)——は、この規律の外で
38
+ 相手の `username` と本文をそのまま残す(読む口でもなく、絡みの台帳でもない)。
35
39
  - **人の代わりに承認しない。** あなた(エージェント)は下書きと digest を人に見せ、
36
40
  人が `--confirm` を打つ。
37
41
 
@@ -46,6 +46,11 @@ def test_behind_remoteはfetchできないとNoneとreason(tmp_path):
46
46
  ["git", "init", "-b", "main", str(broken)], capture_output=True, text=True)
47
47
  assert run_git_init.returncode == 0
48
48
  run_git(str(broken), ["remote", "add", "origin", "/does/not/exist/repo.git"])
49
+ # **git の名前とメールを repo に持たせる**(`conftest.init_git_pair()` と同じ)。
50
+ # 開発機(macOS)は全体の設定があるので通っていたが、GitHub Actions の
51
+ # Linux には無く、`commit` が "Author identity unknown" で落ちた(2026-09-16)。
52
+ run_git(str(broken), ["config", "user.email", "thth-test@example.invalid"])
53
+ run_git(str(broken), ["config", "user.name", "thth-test"])
49
54
  run_git(str(broken), ["commit", "--allow-empty", "-m", "x"])
50
55
 
51
56
  info = writeback.behind_remote(str(broken))
@@ -0,0 +1,175 @@
1
+ """絡みの台帳(`data/sns/engagements/`)を採取の commit に含める(T9-1・
2
+ 照合「X Developer Agreement と自分の泉」検収 1)。
3
+
4
+ **事実**: 投稿の commit(`core.py` の `writeback.commit_and_push(rel_path=原稿)`)は
5
+ 原稿ファイルだけ、採取の commit(`collect.py` の `result["touched"]`)は採取で
6
+ 触ったファイルだけを add する。同席送信(`core._send_locked`)は commit しない。
7
+ `thth/engagements.py::append()` が書く `data/sns/engagements/<YYYY-MM>.ndjson` は
8
+ どこからも add されず、VM の repo に未追跡のまま溜まっていた。
9
+
10
+ 固定するのは 4 つ(発注 T9-1 の (a)〜(d)):
11
+
12
+ (a) `engagements.append` で 1 行書いた repo で `collect` を 1 回 → origin に
13
+ `data/sns/engagements/<月>.ndjson` が push されている。
14
+ (b) 採取で触るものが 0 でも (a) と同じ。
15
+ (c) repo が無い account では git を呼ばない。
16
+ (d) 何も変わっていなければ空の commit を作らない。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import datetime
21
+ import json
22
+ import os
23
+ import subprocess
24
+
25
+ from tests.conftest import init_git_pair, make_queue_text
26
+ from tests.test_collect import FakeAdapter
27
+ from thth import accounts as accounts_mod
28
+ from thth import collect as collect_mod
29
+ from thth import engagements as engagements_mod
30
+ from thth import jst
31
+
32
+ NOW = datetime.datetime(2026, 9, 16, 12, 0, tzinfo=jst.JST)
33
+
34
+
35
+ class NoOpAdapter:
36
+ """能力を何も名乗らない偽アダプタ(`collect_once()` に何も触らせない)。
37
+
38
+ `tests/test_collect_sent.py` の `FakeAdapter` と違い `views` すら持たない
39
+ ——**採取が触るものを 0 にする**ためだけの道具。
40
+ """
41
+
42
+ CAPABILITIES = frozenset()
43
+
44
+ @classmethod
45
+ def capabilities(cls):
46
+ return set(cls.CAPABILITIES)
47
+
48
+ def insights(self, post_id):
49
+ raise AssertionError("能力を名乗っていないのに insights を呼んだ")
50
+
51
+ def conversation(self, post_id, *, since=None):
52
+ raise AssertionError("能力を名乗っていないのに conversation を呼んだ")
53
+
54
+ def account_insights(self, user_id, *, since, until):
55
+ raise AssertionError("能力を名乗っていないのに account_insights を呼んだ")
56
+
57
+
58
+ def _engagement_row(**over):
59
+ row = {
60
+ "schema": engagements_mod.SCHEMA,
61
+ "post_id": "111",
62
+ "reply_to": "222",
63
+ "root_post": None,
64
+ "author_key": None,
65
+ "account": None, # append() 側の既定に任せる
66
+ "medium": "threads",
67
+ "topic": None,
68
+ "form": None,
69
+ "hour_band": "朝",
70
+ "posted_at": "2026-09-16T08:00:00+09:00",
71
+ "found_by": None,
72
+ }
73
+ row.update(over)
74
+ return row
75
+
76
+
77
+ def _in_origin(bare: str) -> str:
78
+ return subprocess.run(
79
+ ["git", "-C", bare, "ls-tree", "-r", "--name-only", "main"],
80
+ capture_output=True, text=True,
81
+ ).stdout
82
+
83
+
84
+ def test_a_絡みの台帳が採取のcommitでpushされる(tmp_path, isolated_account_factory):
85
+ """採取が投稿本体も触る、ふつうの回。絡みの台帳も同じ commit に乗って
86
+ origin まで届く。"""
87
+ pair = init_git_pair(tmp_path, seed_content=make_queue_text({
88
+ "status": "posted", "post_id": "POST1",
89
+ "posted_at": "2026-09-16T10:00:00+09:00"})) # 2 時間前・1h の刻みを跨ぐ
90
+ account = isolated_account_factory(repo_dir=pair["work"], production=True)
91
+
92
+ cfg = accounts_mod.load_account(account["name"])
93
+ engagements_mod.append(cfg, account["name"],
94
+ _engagement_row(account=account["name"]), now=NOW)
95
+
96
+ rc = collect_mod.run_collect(account["name"], adapter=FakeAdapter(), now=NOW,
97
+ log=lambda _l: None)
98
+
99
+ assert rc == 0
100
+ in_origin = _in_origin(pair["bare"])
101
+ assert "data/sns/engagements/2026-09.ndjson" in in_origin, in_origin
102
+ # ついでに、投稿の実測も同じ commit の対象として届いていること
103
+ # (足した処理が既存の touched を壊していないこと)。
104
+ assert "data/sns/insights/posts/POST1.ndjson" in in_origin, in_origin
105
+
106
+
107
+ def test_b_採取で触るものが0でも絡みの台帳はpushされる(tmp_path, isolated_account_factory):
108
+ """`collect_once()` の `touched` が空でも、絡みの台帳の未追跡・変更だけで
109
+ commit する。"""
110
+ pair = init_git_pair(tmp_path, seed_content=make_queue_text({
111
+ "status": "posted", "post_id": "POST1",
112
+ # collect_days(既定 14 日)をとうに過ぎている → posts_seen は 0、
113
+ # touched も空のまま(NoOpAdapter は何も能力を名乗らないので
114
+ # アカウント日次も呼ばれない)。
115
+ "posted_at": "2026-08-01T10:00:00+09:00"}))
116
+ account = isolated_account_factory(repo_dir=pair["work"], production=True)
117
+
118
+ cfg = accounts_mod.load_account(account["name"])
119
+ engagements_mod.append(cfg, account["name"],
120
+ _engagement_row(account=account["name"]), now=NOW)
121
+
122
+ rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
123
+ log=lambda _l: None)
124
+
125
+ assert rc == 0
126
+ in_origin = _in_origin(pair["bare"])
127
+ assert "data/sns/engagements/2026-09.ndjson" in in_origin, in_origin
128
+ # 投稿側は本当に何も触っていないこと(この test が (a) の焼き直しに
129
+ # なっていないことの確認)。
130
+ assert "data/sns/insights/posts/POST1.ndjson" not in in_origin, in_origin
131
+
132
+
133
+ def test_c_repoが無いaccountでは絡みの台帳も含めてgitを呼ばない(
134
+ tmp_path, isolated_account_factory, monkeypatch):
135
+ """`repo_dir` が実在しない台帳(`repos/_none` 相当)では、絡みの台帳の
136
+ 判定そのもの(`_pending_engagement_paths`)を呼ばない——呼べば
137
+ `git status` を repo の外で走らせる危険がある。"""
138
+ account = isolated_account_factory(
139
+ repo_dir=str(tmp_path / "repos" / "_none"), production=True, scheduled=False)
140
+
141
+ def 呼ぶな(*a, **k):
142
+ raise AssertionError("repo が無いのに絡みの台帳の git 判定を呼んだ")
143
+
144
+ monkeypatch.setattr(collect_mod, "_pending_engagement_paths", 呼ぶな)
145
+ monkeypatch.setattr(collect_mod, "_git", 呼ぶな)
146
+ monkeypatch.setattr(collect_mod.writeback, "sync_repo", 呼ぶな)
147
+ monkeypatch.setattr(collect_mod.writeback, "commit_and_push", 呼ぶな)
148
+
149
+ rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
150
+ log=lambda _l: None)
151
+ assert rc == 0
152
+
153
+
154
+ def test_d_変わっていなければ空のcommitを作らない(tmp_path, isolated_account_factory,
155
+ monkeypatch):
156
+ """絡みの台帳に変更が無い回は、`commit_and_push` を一切呼ばない。"""
157
+ pair = init_git_pair(tmp_path, seed_content=make_queue_text({
158
+ "status": "posted", "post_id": "POST1",
159
+ "posted_at": "2026-08-01T10:00:00+09:00"})) # collect_days を過ぎている
160
+ account = isolated_account_factory(repo_dir=pair["work"], production=True)
161
+
162
+ calls = []
163
+ real_commit_and_push = collect_mod.writeback.commit_and_push
164
+
165
+ def 数える(*a, **k):
166
+ calls.append((a, k))
167
+ return real_commit_and_push(*a, **k)
168
+
169
+ monkeypatch.setattr(collect_mod.writeback, "commit_and_push", 数える)
170
+
171
+ rc = collect_mod.run_collect(account["name"], adapter=NoOpAdapter(), now=NOW,
172
+ log=lambda _l: None)
173
+
174
+ assert rc == 0
175
+ assert calls == [], "変更が無いのに commit_and_push を呼んだ"
@@ -0,0 +1,179 @@
1
+ """読む口 4 つ(`thread`・`where`・`who`・`threads_read_cli` の口)が、adapter の
2
+ 素の `RuntimeError` で traceback にならない(T9-2・照合「X Developer Agreement
3
+ と自分の泉」検収 5)。
4
+
5
+ **事実**: `thth/adapters/base.py` の `AdapterError` は `RuntimeError` の子。
6
+ Bluesky(`bluesky.py` の `_request` 周り・`session()` の 367・374 行、`_xrpc()` の
7
+ 260・262 行)は素の `RuntimeError` を投げる箇所がある。`thth/thread_read.py`・
8
+ `thth/where_cli.py`・`thth/who_cli.py`・`thth/threads_read_cli.py::_run` はどれも
9
+ `AdapterError` と `PermissionMissing` だけを捕まえていたので、素の `RuntimeError`
10
+ は traceback になっていた。
11
+
12
+ ここでは実際の Bluesky・Threads の通信は起こさず、`adapters.make_adapter()` を
13
+ 差し替えた偽アダプタで「adapter が素の `RuntimeError` を投げたら」を作る——
14
+ 4 つの口それぞれの捕捉の**位置**(`thread_read.cmd_thread`・
15
+ `where_cli._account_node` の語ごとの try・`who_cli._profile_for`・
16
+ `threads_read_cli._run`)を直に確かめる。
17
+
18
+ 固定するのは:
19
+ - `thread`・`threads_read_cli`(`topics --search`): rc=1・`--json` は
20
+ `{"error", "account"}`・**adapter の例外の型は変えていない**ので
21
+ `AdapterError` の枝はそのまま通る。
22
+ - `where`・`who`: その account(語・プロフィール)の `cannot_say` に入り、
23
+ 他の語・他の account は続く(`AdapterError` と同じ既存の筋のまま)。
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+
30
+ import pytest
31
+
32
+ from thth import accounts as accounts_mod
33
+ from thth import adapters as adapters_mod
34
+ from thth import thread_read as thread_read_mod
35
+ from thth import threads_read_cli as threads_read_cli_mod
36
+ from thth import where_cli as where_cli_mod
37
+ from thth import who_cli as who_cli_mod
38
+
39
+ BARE_MESSAGE = "素の RuntimeError(fake・T9-2・bluesky._request 相当)"
40
+
41
+
42
+ class _BareRuntimeErrorAdapter:
43
+ """能力だけ答え、実際に叩かれたら**`AdapterError` ではない**素の
44
+ `RuntimeError` を投げる偽アダプタ。Bluesky の `_request`/`session()` が
45
+ 本番で投げる形を模す。"""
46
+
47
+ def capabilities(self):
48
+ return {"thread_read", "keyword_search", "profile_lookup", "mentions"}
49
+
50
+ def fetch_post(self, post_id):
51
+ raise RuntimeError(BARE_MESSAGE)
52
+
53
+ def conversation(self, post_id, *, since=None):
54
+ raise RuntimeError(BARE_MESSAGE)
55
+
56
+ def keyword_search(self, word, *, search_type, limit):
57
+ raise RuntimeError(BARE_MESSAGE)
58
+
59
+ def profile_lookup(self, username):
60
+ raise RuntimeError(BARE_MESSAGE)
61
+
62
+ def mentions(self, *, since=None):
63
+ raise RuntimeError(BARE_MESSAGE)
64
+
65
+ def granted_scopes(self):
66
+ return []
67
+
68
+
69
+ def _write_token(path):
70
+ with open(path, "w", encoding="utf-8") as f:
71
+ json.dump({"access_token": "FAKE-SECRET", "user_id": "999999",
72
+ "username": "nigamilab", "scopes": None,
73
+ "obtained_at": "2026-09-16T09:00:00+09:00"}, f)
74
+
75
+
76
+ @pytest.fixture
77
+ def bare_runtimeerror_account(tmp_path, isolated_account_factory, monkeypatch):
78
+ """Threads 台帳(4 つの capability を全部持つ実クラスの検査は通す)だが、
79
+ `adapters.make_adapter()` を差し替えて、実際に返す adapter は素の
80
+ `RuntimeError` しか投げない偽物にする。"""
81
+ token_path = str(tmp_path / "a.token")
82
+ _write_token(token_path)
83
+ account = isolated_account_factory(token=token_path, handle="nigamilab")
84
+ monkeypatch.setattr(adapters_mod, "make_adapter",
85
+ lambda cfg, token: _BareRuntimeErrorAdapter())
86
+ return account
87
+
88
+
89
+ # ---------------------------------------------------------------- thread_read
90
+
91
+ def test_thread読む口はrc1でtracebackにならない(bare_runtimeerror_account, capsys):
92
+ args = argparse.Namespace(account=bare_runtimeerror_account["name"], post_id="POST1",
93
+ since=None, max_messages=thread_read_mod.DEFAULT_MAX_MESSAGES,
94
+ json=True)
95
+ rc = thread_read_mod.cmd_thread(args)
96
+ out = capsys.readouterr()
97
+
98
+ assert rc == 1
99
+ assert "Traceback" not in out.err
100
+ payload = json.loads(out.out)
101
+ assert set(payload) == {"error", "account"}
102
+ assert payload["account"] == bare_runtimeerror_account["name"]
103
+ assert BARE_MESSAGE in payload["error"]
104
+
105
+
106
+ # ------------------------------------------------------------ threads_read_cli
107
+
108
+ def test_threads_read_cliの口はrc1でtracebackにならない(bare_runtimeerror_account, capsys):
109
+ args = argparse.Namespace(account=bare_runtimeerror_account["name"], search="語",
110
+ recent=False, limit=25, json=True)
111
+ rc = threads_read_cli_mod.cmd_topics_search(args)
112
+ out = capsys.readouterr()
113
+
114
+ assert rc == 1
115
+ assert "Traceback" not in out.err
116
+ payload = json.loads(out.out)
117
+ assert set(payload) == {"error", "account"}
118
+ assert payload["account"] == bare_runtimeerror_account["name"]
119
+ assert BARE_MESSAGE in payload["error"]
120
+
121
+
122
+ # --------------------------------------------------------------------- where
123
+
124
+ def test_where読む口はcannot_sayに入り他の語は続く(bare_runtimeerror_account, capsys):
125
+ args = argparse.Namespace(project=None,
126
+ targets=[bare_runtimeerror_account["name"], "苦い", "コーヒー"],
127
+ recent=False, limit=where_cli_mod.DEFAULT_LIMIT, json=True)
128
+ rc = where_cli_mod.cmd_where(args)
129
+ out = capsys.readouterr()
130
+
131
+ assert rc == 0, out.err # `AdapterError` と同じ筋(account 単位の失敗にしない)
132
+ assert "Traceback" not in out.err
133
+ result = json.loads(out.out)
134
+ node = result["by_account"][bare_runtimeerror_account["name"]]
135
+ # 2 語とも by_word には入らず、node の cannot_say に落ちる(PermissionMissing/
136
+ # AdapterError と同じ扱い・T9-2 で RuntimeError も同じ列に並んだ)。
137
+ assert node["by_word"] == {}
138
+ assert any("苦い" in line and BARE_MESSAGE in line for line in node["cannot_say"])
139
+ assert any("コーヒー" in line and BARE_MESSAGE in line for line in node["cannot_say"])
140
+
141
+
142
+ # ---------------------------------------------------------------------- who
143
+
144
+ def test_whoの読む口はcannot_sayに入る(bare_runtimeerror_account, capsys):
145
+ args = argparse.Namespace(project=None,
146
+ targets=[bare_runtimeerror_account["name"], "@bob"],
147
+ profile=True, json=True)
148
+ rc = who_cli_mod.cmd_who(args)
149
+ out = capsys.readouterr()
150
+
151
+ assert rc == 0, out.err
152
+ assert "Traceback" not in out.err
153
+ result = json.loads(out.out)
154
+ assert result["profile"] is None
155
+ assert any(BARE_MESSAGE in line for line in result["cannot_say"])
156
+
157
+
158
+ # ----------------------------------------------------- adapter の例外の型は不変
159
+
160
+ def test_AdapterErrorの枝は素のRuntimeErrorに食われない():
161
+ """`except AdapterError` が先・`except RuntimeError` が後——`AdapterError`
162
+ は `RuntimeError` の子なので、順序を間違えると `AdapterError` の枝が
163
+ 死んでいてもテストは気づかない。ここは 4 箇所のソースを直に読んで、
164
+ `AdapterError` の except が `RuntimeError` の except より**前**にあることを
165
+ 確かめる(読み違いで壊す変異にも強くする)。"""
166
+ import inspect
167
+
168
+ for mod, func_name in (
169
+ (thread_read_mod, "cmd_thread"),
170
+ (threads_read_cli_mod, "_run"),
171
+ (where_cli_mod, "_account_node"),
172
+ (who_cli_mod, "_profile_for"),
173
+ ):
174
+ src = inspect.getsource(getattr(mod, func_name))
175
+ adapter_error_at = src.index("except adapter_base.AdapterError")
176
+ runtime_error_at = src.index("except RuntimeError")
177
+ assert adapter_error_at < runtime_error_at, (
178
+ f"{mod.__name__}.{func_name}: AdapterError の except が "
179
+ f"RuntimeError より後にある——AdapterError の枝が死ぬ")
@@ -99,3 +99,29 @@ def test_泉に落とすかどうかの節が削られている():
99
99
  """T4-1「『泉に落とすかどうか(§4)』の節は削る」。"""
100
100
  text = _read()
101
101
  assert "泉に落とすかどうか" not in text
102
+
103
+
104
+ def _bullets(text: str) -> list:
105
+ """トップレベルの `- ` 箇条書きを 1 項目 1 要素に割る。
106
+
107
+ 続きの行(インデントされた行)は直前の項目にくっつく——1 つの箇条書き
108
+ 項目が複数行にまたがっていても 1 ブロックとして見られるように。
109
+ """
110
+ return re.split(r"\n(?=- )", text)
111
+
112
+
113
+ def test_replies台帳が相手のusernameと本文を残すと書かれている():
114
+ """T9-3: 自分の投稿への返信の台帳(`thth collect` が採り、`thth replies` で
115
+ 読む)は相手の `username` と本文を残す——「何を絶対にしないか」の読む口の
116
+ 項(`thread_read`・`where_to_appear`・`who_is_this` は何も保存しない・
117
+ 絡みの台帳は自分の行為と反応だけ)と混同されないよう、この事実を書く
118
+ (照合「X Developer Agreement と自分の泉」検収 2)。
119
+
120
+ **言い方を固定しすぎない検査**: 特定の文言を assert するのではなく、
121
+ `replies` と `username` が**同じ箇条書き項目**に出てくることだけを見る。
122
+ """
123
+ text = _read()
124
+ bullets = _bullets(text)
125
+ hit = [b for b in bullets if "replies" in b and "username" in b]
126
+ assert hit, ("「replies」と「username」が同じ箇条書き項目に出てくる"
127
+ "ブロックがありません")
@@ -0,0 +1 @@
1
+ 2.3.3
@@ -132,6 +132,54 @@ def pending_paths(repo_dir: str, account_cfg: dict) -> list:
132
132
  return sorted(out)
133
133
 
134
134
 
135
+ def _pending_engagement_paths(repo_dir: str, account_cfg: dict, account_name: str) -> list:
136
+ """絡みの台帳(`data/sns/engagements/*.ndjson`)のうち、未追跡か変更が
137
+ あるものを絶対パスで返す(T9-1・照合「X Developer Agreement と自分の泉」
138
+ 検収 1)。
139
+
140
+ `collect_once()` の `touched` は「今回の採取で触ったファイル」だけを
141
+ 数える。絡みの台帳(`thth/engagements.py::append()`)は**公開が確定した
142
+ 直後**に書かれ、それは `thth core.py` の queue 経由の投稿だけでなく
143
+ `_send_locked`(同席送信)でも起きる——だが `_send_locked` は commit しない
144
+ (`core.py:707,759-780`)。結果、絡みの行は VM の repo に未追跡のまま
145
+ 溜まり、**同期(`sync_repo`)は未追跡を見ないので止まらないが、git には
146
+ 一度も入らない**。
147
+
148
+ ここで `git status --porcelain` を見て、採取自身が何も触らなくても
149
+ 絡みの台帳の変更を拾う(`writeback.pending_paths()` と同じ流儀だが、
150
+ 見る先を `data/sns` 全体ではなく **engagements だけ**に絞る——他の
151
+ `data/sns` 配下の変更を二重に拾わないため)。
152
+ """
153
+ if not repo_dir or not os.path.isdir(repo_dir):
154
+ return []
155
+ dirs = accounts_mod.data_dirs(account_cfg, account_name)
156
+ engagements_dir = dirs["engagements"]
157
+ if not os.path.isdir(engagements_dir):
158
+ return []
159
+ rel_dir = os.path.relpath(os.path.realpath(engagements_dir), os.path.realpath(repo_dir))
160
+ if rel_dir == os.pardir or rel_dir.startswith(os.pardir + os.sep):
161
+ # engagements が repo の外を指す異常系(state 側の置き場と repo_dir が
162
+ # 食い違っている)。git には触らない。
163
+ return []
164
+ # **`--untracked-files=all` が要る。** 既定の `git status --porcelain` は
165
+ # 中身が丸ごと未追跡なディレクトリを `?? data/sns/engagements/`(末尾
166
+ # スラッシュ付きの 1 行)にまとめて返し、個々のファイル名を出さない。
167
+ # `engagements/` は採取が repo に初めて作る側なので、**最初の 1 回は
168
+ # ほぼ必ずこの「ディレクトリごと未追跡」の形**になる——`.ndjson` で
169
+ # 終わらない行として弾かれ、絶対に拾えなかった(この関数を書いた本人が
170
+ # 変異テストで踏んだ)。
171
+ status = _git(repo_dir, ["status", "--porcelain", "--untracked-files=all",
172
+ "--", rel_dir])
173
+ if status.returncode != 0:
174
+ return []
175
+ out = []
176
+ for line in status.stdout.splitlines():
177
+ path = line[3:].strip()
178
+ if path and path.endswith(".ndjson"):
179
+ out.append(os.path.join(repo_dir, path))
180
+ return sorted(out)
181
+
182
+
135
183
  def _read_ndjson_strict(path: str) -> tuple:
136
184
  """`(行, 壊れているか)`。**壊れた行を黙って飛ばさない。**
137
185
 
@@ -1004,9 +1052,15 @@ def _collect_entry(account_name: str, *, adapter, now, log, info: dict) -> int:
1004
1052
  result = collect_once(account_name, adapter=adapter, now=now, log=log)
1005
1053
  _note(info, result)
1006
1054
 
1007
- if result["touched"]:
1055
+ # T9-1: 絡みの台帳(`_send_locked` が書いた行も含む)は `collect_once()`
1056
+ # の `touched` に一度も現れない。採取で他に何も触らなくても、絡みの
1057
+ # 台帳に未追跡・変更があれば同じ commit に含める。
1058
+ engagement_paths = _pending_engagement_paths(repo_dir, account_cfg, account_name)
1059
+ all_touched = sorted(set(result["touched"]) | set(engagement_paths))
1060
+
1061
+ if all_touched:
1008
1062
  rel = [os.path.relpath(os.path.realpath(p), os.path.realpath(repo_dir))
1009
- for p in result["touched"]]
1063
+ for p in all_touched]
1010
1064
  pushed, push_err = writeback.commit_and_push(
1011
1065
  repo_dir, rel_path=rel,
1012
1066
  message=f"収集: {len(rel)} ファイル({account_name})")
@@ -474,6 +474,22 @@ def cmd_thread(args) -> int:
474
474
  else:
475
475
  print(message, file=sys.stderr)
476
476
  return 1
477
+ except RuntimeError as e:
478
+ # **`AdapterError` ではない素の `RuntimeError`**(Bluesky の `_request`
479
+ # 周りなど)を、`AdapterError` と同じ出し方で受ける(T9-2・照合
480
+ # 「X Developer Agreement と自分の泉」検収 5)。`AdapterError` は
481
+ # `RuntimeError` の子なので、この節は上の `except AdapterError` を
482
+ # すり抜けたものだけを拾う。**adapter が投げる例外の型は変えない**
483
+ # ——ここは受け口を増やすだけ。
484
+ message = f"{args.account}: {redact_mod.redact(str(e))}"
485
+ _record_run(args.account, media=media, post_id=args.post_id, messages=None,
486
+ truncated=False, status="error", error=message)
487
+ if as_json:
488
+ print(json.dumps({"error": message, "account": args.account},
489
+ ensure_ascii=False, indent=2))
490
+ else:
491
+ print(message, file=sys.stderr)
492
+ return 1
477
493
 
478
494
  _record_run(args.account, media=media, post_id=args.post_id,
479
495
  messages=result["counts"]["messages"],
@@ -246,6 +246,19 @@ def _run(account: str, *, capability: str, call, as_json: bool, render,
246
246
  else:
247
247
  print(message, file=sys.stderr)
248
248
  return 1
249
+ except RuntimeError as e:
250
+ # **`AdapterError` ではない素の `RuntimeError`**(Bluesky の `_request`
251
+ # 周りなど)を、`AdapterError` と同じ出し方で受ける(T9-2)。
252
+ # `AdapterError` は `RuntimeError` の子なので、上の
253
+ # `except AdapterError` をすり抜けたものだけがここに来る。**adapter が
254
+ # 投げる例外の型は変えない**——ここは受け口を増やすだけ。
255
+ message = f"{account}: {redact_mod.redact(str(e))}"
256
+ if as_json:
257
+ print(json.dumps({"error": message, "account": account},
258
+ ensure_ascii=False, indent=2))
259
+ else:
260
+ print(message, file=sys.stderr)
261
+ return 1
249
262
  return render(result)
250
263
 
251
264
 
@@ -163,6 +163,16 @@ def _account_node(account_name: str, words: list, *, search_type: str,
163
163
  except adapter_base.AdapterError as e:
164
164
  node_cannot_say.append(f"{word}: {redact_mod.redact(str(e))}")
165
165
  continue
166
+ except RuntimeError as e:
167
+ # **`AdapterError` ではない素の `RuntimeError`**(Bluesky の
168
+ # `_request` 周りなど)を、`AdapterError` と同じ出し方で受ける
169
+ # (T9-2)。`AdapterError` は `RuntimeError` の子なので、上の
170
+ # `except AdapterError` をすり抜けたものだけがここに来る。この
171
+ # account 全体を落とさず、その語だけ `cannot_say` に流して他の
172
+ # 語・他の account を続ける(`--project` で 1 account だけ落ちて
173
+ # も他が出る、という既存の筋と同じ)。
174
+ node_cannot_say.append(f"{word}: {redact_mod.redact(str(e))}")
175
+ continue
166
176
 
167
177
  material = threads_read_cli_mod.search_material(
168
178
  rows, q=word, search_type=search_type, limit=limit)
@@ -95,6 +95,14 @@ def _profile_for(account_cfg: dict, account_name: str, *, username: str | None):
95
95
  return None, _permission_message(account_name, adapter, e)
96
96
  except adapter_base.AdapterError as e:
97
97
  return None, redact_mod.redact(str(e))
98
+ except RuntimeError as e:
99
+ # **`AdapterError` ではない素の `RuntimeError`**(Bluesky の
100
+ # `_request` 周りなど)を、`AdapterError` と同じ出し方で受ける
101
+ # (T9-2)。`AdapterError` は `RuntimeError` の子なので、上の
102
+ # `except AdapterError` をすり抜けたものだけがここに来る。呼び出し側
103
+ # (`_account_node`)はこれを `cannot_say` に足して account 全体は
104
+ # 落とさない。
105
+ return None, redact_mod.redact(str(e))
98
106
  return profile, None
99
107
 
100
108
 
thth-2.3.2/thth/VERSION DELETED
@@ -1 +0,0 @@
1
- 2.3.2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes