thth 4.5.0__tar.gz → 4.5.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (777) hide show
  1. {thth-4.5.0 → thth-4.5.2}/PKG-INFO +2 -1
  2. {thth-4.5.0 → thth-4.5.2}/README.md +1 -0
  3. {thth-4.5.0 → thth-4.5.2}/tests/test_lock.py +3 -3
  4. {thth-4.5.0 → thth-4.5.2}/tests/test_repo_lock.py +6 -4
  5. {thth-4.5.0 → thth-4.5.2}/tests/test_report_isolation.py +18 -0
  6. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_read_coordination.py +10 -5
  7. {thth-4.5.0 → thth-4.5.2}/tests/test_v40_contract_json.py +13 -8
  8. thth-4.5.2/tests/test_v442_sync_lookup.py +122 -0
  9. thth-4.5.2/tests/test_v452_schedule_stock.py +84 -0
  10. thth-4.5.2/thth/VERSION +1 -0
  11. {thth-4.5.0 → thth-4.5.2}/thth/cli.py +103 -7
  12. {thth-4.5.0 → thth-4.5.2}/thth/events_cli.py +53 -2
  13. {thth-4.5.0 → thth-4.5.2}/thth/report_isolation.py +6 -1
  14. {thth-4.5.0 → thth-4.5.2}/thth/writeback.py +29 -0
  15. thth-4.5.0/thth/VERSION +0 -1
  16. {thth-4.5.0 → thth-4.5.2}/.gitignore +0 -0
  17. {thth-4.5.0 → thth-4.5.2}/LICENSE +0 -0
  18. {thth-4.5.0 → thth-4.5.2}/accounts.example/bluesky.json +0 -0
  19. {thth-4.5.0 → thth-4.5.2}/accounts.example/hatenablog.json +0 -0
  20. {thth-4.5.0 → thth-4.5.2}/accounts.example/mastodon.json +0 -0
  21. {thth-4.5.0 → thth-4.5.2}/accounts.example/principal-hatena.json +0 -0
  22. {thth-4.5.0 → thth-4.5.2}/accounts.example/threads.json +0 -0
  23. {thth-4.5.0 → thth-4.5.2}/accounts.example/x.json +0 -0
  24. {thth-4.5.0 → thth-4.5.2}/bin/thth +0 -0
  25. {thth-4.5.0 → thth-4.5.2}/bin/thth-remote +0 -0
  26. {thth-4.5.0 → thth-4.5.2}/bin/thth-run +0 -0
  27. {thth-4.5.0 → thth-4.5.2}/mcp/server.py +0 -0
  28. {thth-4.5.0 → thth-4.5.2}/pyproject.toml +0 -0
  29. {thth-4.5.0 → thth-4.5.2}/skills/thth/SKILL.md +0 -0
  30. {thth-4.5.0 → thth-4.5.2}/skills/thth-admin/SKILL.md +0 -0
  31. {thth-4.5.0 → thth-4.5.2}/systemd/thth@.service +0 -0
  32. {thth-4.5.0 → thth-4.5.2}/tests/__init__.py +0 -0
  33. {thth-4.5.0 → thth-4.5.2}/tests/conftest.py +0 -0
  34. {thth-4.5.0 → thth-4.5.2}/tests/coordination_snapshot.py +0 -0
  35. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/GraphemeBreakTest-17.0.0.txt +0 -0
  36. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/build_injection_eval.py +0 -0
  37. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/callback/worker_responses.json +0 -0
  38. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/injection_eval.json +0 -0
  39. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/limits_stage3/README.txt +0 -0
  40. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/limits_stage3/ledgers/vm-kopicha-hatenablog.json +0 -0
  41. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/limits_stage3/ledgers/vm-kopicha-x.json +0 -0
  42. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/limits_stage3/ledgers/vm-music-threads.json +0 -0
  43. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/limits_stage3/principals/vm-kopicha.json +0 -0
  44. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/twitter_text_v3_weighted.json +0 -0
  45. {thth-4.5.0 → thth-4.5.2}/tests/fixtures/umami-bile.md +0 -0
  46. {thth-4.5.0 → thth-4.5.2}/tests/hatena_s1_flow_helpers.py +0 -0
  47. {thth-4.5.0 → thth-4.5.2}/tests/hatena_s1_helpers.py +0 -0
  48. {thth-4.5.0 → thth-4.5.2}/tests/hatena_s2_helpers.py +0 -0
  49. {thth-4.5.0 → thth-4.5.2}/tests/hatena_s2_http.py +0 -0
  50. {thth-4.5.0 → thth-4.5.2}/tests/hatena_s3_helpers.py +0 -0
  51. {thth-4.5.0 → thth-4.5.2}/tests/hatena_s4_helpers.py +0 -0
  52. {thth-4.5.0 → thth-4.5.2}/tests/hatena_s5_helpers.py +0 -0
  53. {thth-4.5.0 → thth-4.5.2}/tests/hatena_s6_helpers.py +0 -0
  54. {thth-4.5.0 → thth-4.5.2}/tests/helpers/__init__.py +0 -0
  55. {thth-4.5.0 → thth-4.5.2}/tests/helpers/fake_oauth_server.py +0 -0
  56. {thth-4.5.0 → thth-4.5.2}/tests/helpers/fake_push_adapter.py +0 -0
  57. {thth-4.5.0 → thth-4.5.2}/tests/helpers/fake_redirect_server.py +0 -0
  58. {thth-4.5.0 → thth-4.5.2}/tests/helpers/fake_search_adapter.py +0 -0
  59. {thth-4.5.0 → thth-4.5.2}/tests/helpers/fake_threads_reader.py +0 -0
  60. {thth-4.5.0 → thth-4.5.2}/tests/helpers/fake_threads_status.py +0 -0
  61. {thth-4.5.0 → thth-4.5.2}/tests/helpers/fake_threads_writes.py +0 -0
  62. {thth-4.5.0 → thth-4.5.2}/tests/helpers/fake_x.py +0 -0
  63. {thth-4.5.0 → thth-4.5.2}/tests/helpers/hold_lock.py +0 -0
  64. {thth-4.5.0 → thth-4.5.2}/tests/helpers/hold_repo_lock.py +0 -0
  65. {thth-4.5.0 → thth-4.5.2}/tests/helpers/worker_probe.mjs +0 -0
  66. {thth-4.5.0 → thth-4.5.2}/tests/test_27_acceptance_fixes.py +0 -0
  67. {thth-4.5.0 → thth-4.5.2}/tests/test_account_name_check.py +0 -0
  68. {thth-4.5.0 → thth-4.5.2}/tests/test_account_report.py +0 -0
  69. {thth-4.5.0 → thth-4.5.2}/tests/test_accounts_dir.py +0 -0
  70. {thth-4.5.0 → thth-4.5.2}/tests/test_adapter_boundary.py +0 -0
  71. {thth-4.5.0 → thth-4.5.2}/tests/test_admin_http_mcp.py +0 -0
  72. {thth-4.5.0 → thth-4.5.2}/tests/test_admin_log.py +0 -0
  73. {thth-4.5.0 → thth-4.5.2}/tests/test_admin_notifications.py +0 -0
  74. {thth-4.5.0 → thth-4.5.2}/tests/test_admin_repairs.py +0 -0
  75. {thth-4.5.0 → thth-4.5.2}/tests/test_admin_report.py +0 -0
  76. {thth-4.5.0 → thth-4.5.2}/tests/test_after_cli.py +0 -0
  77. {thth-4.5.0 → thth-4.5.2}/tests/test_analytics_comparison.py +0 -0
  78. {thth-4.5.0 → thth-4.5.2}/tests/test_analytics_report.py +0 -0
  79. {thth-4.5.0 → thth-4.5.2}/tests/test_api_diagnostic.py +0 -0
  80. {thth-4.5.0 → thth-4.5.2}/tests/test_api_error_body.py +0 -0
  81. {thth-4.5.0 → thth-4.5.2}/tests/test_app_env_cli.py +0 -0
  82. {thth-4.5.0 → thth-4.5.2}/tests/test_approval.py +0 -0
  83. {thth-4.5.0 → thth-4.5.2}/tests/test_approve_cmd.py +0 -0
  84. {thth-4.5.0 → thth-4.5.2}/tests/test_ask.py +0 -0
  85. {thth-4.5.0 → thth-4.5.2}/tests/test_assess.py +0 -0
  86. {thth-4.5.0 → thth-4.5.2}/tests/test_atlas_fifth_review.py +0 -0
  87. {thth-4.5.0 → thth-4.5.2}/tests/test_atlas_fourth_review.py +0 -0
  88. {thth-4.5.0 → thth-4.5.2}/tests/test_atlas_review.py +0 -0
  89. {thth-4.5.0 → thth-4.5.2}/tests/test_atlas_second_review.py +0 -0
  90. {thth-4.5.0 → thth-4.5.2}/tests/test_atlas_third_review.py +0 -0
  91. {thth-4.5.0 → thth-4.5.2}/tests/test_audit1_fixes.py +0 -0
  92. {thth-4.5.0 → thth-4.5.2}/tests/test_audit2_fixes.py +0 -0
  93. {thth-4.5.0 → thth-4.5.2}/tests/test_audit_b_counterexamples.py +0 -0
  94. {thth-4.5.0 → thth-4.5.2}/tests/test_audit_b_m1_semantics.py +0 -0
  95. {thth-4.5.0 → thth-4.5.2}/tests/test_audit_b_n1_depth.py +0 -0
  96. {thth-4.5.0 → thth-4.5.2}/tests/test_audit_b_r1_closure.py +0 -0
  97. {thth-4.5.0 → thth-4.5.2}/tests/test_audit_v201.py +0 -0
  98. {thth-4.5.0 → thth-4.5.2}/tests/test_audit_v23_details.py +0 -0
  99. {thth-4.5.0 → thth-4.5.2}/tests/test_auth.py +0 -0
  100. {thth-4.5.0 → thth-4.5.2}/tests/test_auth_identity_required.py +0 -0
  101. {thth-4.5.0 → thth-4.5.2}/tests/test_b17_remote_reads.py +0 -0
  102. {thth-4.5.0 → thth-4.5.2}/tests/test_behind_remote.py +0 -0
  103. {thth-4.5.0 → thth-4.5.2}/tests/test_bluesky_adapter.py +0 -0
  104. {thth-4.5.0 → thth-4.5.2}/tests/test_bluesky_secret_scrub.py +0 -0
  105. {thth-4.5.0 → thth-4.5.2}/tests/test_board_needs_review.py +0 -0
  106. {thth-4.5.0 → thth-4.5.2}/tests/test_board_running.py +0 -0
  107. {thth-4.5.0 → thth-4.5.2}/tests/test_branch_replies.py +0 -0
  108. {thth-4.5.0 → thth-4.5.2}/tests/test_bundle.py +0 -0
  109. {thth-4.5.0 → thth-4.5.2}/tests/test_bundle_not_type_mismatch.py +0 -0
  110. {thth-4.5.0 → thth-4.5.2}/tests/test_charcount.py +0 -0
  111. {thth-4.5.0 → thth-4.5.2}/tests/test_cli_entrypoint.py +0 -0
  112. {thth-4.5.0 → thth-4.5.2}/tests/test_cli_help.py +0 -0
  113. {thth-4.5.0 → thth-4.5.2}/tests/test_cli_vm_path.py +0 -0
  114. {thth-4.5.0 → thth-4.5.2}/tests/test_collect.py +0 -0
  115. {thth-4.5.0 → thth-4.5.2}/tests/test_collect_engagements.py +0 -0
  116. {thth-4.5.0 → thth-4.5.2}/tests/test_collect_runs.py +0 -0
  117. {thth-4.5.0 → thth-4.5.2}/tests/test_collect_sent.py +0 -0
  118. {thth-4.5.0 → thth-4.5.2}/tests/test_comparability.py +0 -0
  119. {thth-4.5.0 → thth-4.5.2}/tests/test_control_chars_refused.py +0 -0
  120. {thth-4.5.0 → thth-4.5.2}/tests/test_conversation_paging.py +0 -0
  121. {thth-4.5.0 → thth-4.5.2}/tests/test_count_for.py +0 -0
  122. {thth-4.5.0 → thth-4.5.2}/tests/test_docs_links.py +0 -0
  123. {thth-4.5.0 → thth-4.5.2}/tests/test_doctor.py +0 -0
  124. {thth-4.5.0 → thth-4.5.2}/tests/test_doctor_all_permissions.py +0 -0
  125. {thth-4.5.0 → thth-4.5.2}/tests/test_doctor_next_step.py +0 -0
  126. {thth-4.5.0 → thth-4.5.2}/tests/test_duplicate_front_matter_keys.py +0 -0
  127. {thth-4.5.0 → thth-4.5.2}/tests/test_editorial_audit.py +0 -0
  128. {thth-4.5.0 → thth-4.5.2}/tests/test_editorial_n3_n4.py +0 -0
  129. {thth-4.5.0 → thth-4.5.2}/tests/test_engagements.py +0 -0
  130. {thth-4.5.0 → thth-4.5.2}/tests/test_external_acceptance.py +0 -0
  131. {thth-4.5.0 → thth-4.5.2}/tests/test_fake_api.py +0 -0
  132. {thth-4.5.0 → thth-4.5.2}/tests/test_form_spec.py +0 -0
  133. {thth-4.5.0 → thth-4.5.2}/tests/test_form_spec_cli.py +0 -0
  134. {thth-4.5.0 → thth-4.5.2}/tests/test_forms.py +0 -0
  135. {thth-4.5.0 → thth-4.5.2}/tests/test_fresh_install.py +0 -0
  136. {thth-4.5.0 → thth-4.5.2}/tests/test_front_matter_injection.py +0 -0
  137. {thth-4.5.0 → thth-4.5.2}/tests/test_healthcheck_notifications.py +0 -0
  138. {thth-4.5.0 → thth-4.5.2}/tests/test_hypotheses.py +0 -0
  139. {thth-4.5.0 → thth-4.5.2}/tests/test_hypothesis_boundaries.py +0 -0
  140. {thth-4.5.0 → thth-4.5.2}/tests/test_hypothesis_units.py +0 -0
  141. {thth-4.5.0 → thth-4.5.2}/tests/test_impact_cli.py +0 -0
  142. {thth-4.5.0 → thth-4.5.2}/tests/test_inbox_granted_scopes.py +0 -0
  143. {thth-4.5.0 → thth-4.5.2}/tests/test_incident_notifications.py +0 -0
  144. {thth-4.5.0 → thth-4.5.2}/tests/test_independent_thread_review.py +0 -0
  145. {thth-4.5.0 → thth-4.5.2}/tests/test_independent_topic_review.py +0 -0
  146. {thth-4.5.0 → thth-4.5.2}/tests/test_independent_topic_review_r2.py +0 -0
  147. {thth-4.5.0 → thth-4.5.2}/tests/test_inflight.py +0 -0
  148. {thth-4.5.0 → thth-4.5.2}/tests/test_inflight_tag_body_hash.py +0 -0
  149. {thth-4.5.0 → thth-4.5.2}/tests/test_jst.py +0 -0
  150. {thth-4.5.0 → thth-4.5.2}/tests/test_limits_policy.py +0 -0
  151. {thth-4.5.0 → thth-4.5.2}/tests/test_limits_stage2.py +0 -0
  152. {thth-4.5.0 → thth-4.5.2}/tests/test_limits_stage3.py +0 -0
  153. {thth-4.5.0 → thth-4.5.2}/tests/test_limits_stage4.py +0 -0
  154. {thth-4.5.0 → thth-4.5.2}/tests/test_lint.py +0 -0
  155. {thth-4.5.0 → thth-4.5.2}/tests/test_list_projections.py +0 -0
  156. {thth-4.5.0 → thth-4.5.2}/tests/test_llm_trial.py +0 -0
  157. {thth-4.5.0 → thth-4.5.2}/tests/test_llm_trial_engage.py +0 -0
  158. {thth-4.5.0 → thth-4.5.2}/tests/test_m2_independent.py +0 -0
  159. {thth-4.5.0 → thth-4.5.2}/tests/test_maintain.py +0 -0
  160. {thth-4.5.0 → thth-4.5.2}/tests/test_mastodon_adapter.py +0 -0
  161. {thth-4.5.0 → thth-4.5.2}/tests/test_mastodon_visibility.py +0 -0
  162. {thth-4.5.0 → thth-4.5.2}/tests/test_max_per_run.py +0 -0
  163. {thth-4.5.0 → thth-4.5.2}/tests/test_mcp.py +0 -0
  164. {thth-4.5.0 → thth-4.5.2}/tests/test_mcp_robustness.py +0 -0
  165. {thth-4.5.0 → thth-4.5.2}/tests/test_measured.py +0 -0
  166. {thth-4.5.0 → thth-4.5.2}/tests/test_media_accounts.py +0 -0
  167. {thth-4.5.0 → thth-4.5.2}/tests/test_media_next_steps.py +0 -0
  168. {thth-4.5.0 → thth-4.5.2}/tests/test_media_roundtrip.py +0 -0
  169. {thth-4.5.0 → thth-4.5.2}/tests/test_media_traits.py +0 -0
  170. {thth-4.5.0 → thth-4.5.2}/tests/test_mismatch_diagnostics.py +0 -0
  171. {thth-4.5.0 → thth-4.5.2}/tests/test_n5_reply_null_element.py +0 -0
  172. {thth-4.5.0 → thth-4.5.2}/tests/test_n6_replies_incomplete_ledger.py +0 -0
  173. {thth-4.5.0 → thth-4.5.2}/tests/test_n7_doctor_error_body.py +0 -0
  174. {thth-4.5.0 → thth-4.5.2}/tests/test_no_limits.py +0 -0
  175. {thth-4.5.0 → thth-4.5.2}/tests/test_observation_provenance.py +0 -0
  176. {thth-4.5.0 → thth-4.5.2}/tests/test_operations_handoff.py +0 -0
  177. {thth-4.5.0 → thth-4.5.2}/tests/test_packaging.py +0 -0
  178. {thth-4.5.0 → thth-4.5.2}/tests/test_preview.py +0 -0
  179. {thth-4.5.0 → thth-4.5.2}/tests/test_production_mode.py +0 -0
  180. {thth-4.5.0 → thth-4.5.2}/tests/test_pull_cli.py +0 -0
  181. {thth-4.5.0 → thth-4.5.2}/tests/test_pulse.py +0 -0
  182. {thth-4.5.0 → thth-4.5.2}/tests/test_queue.py +0 -0
  183. {thth-4.5.0 → thth-4.5.2}/tests/test_quiet_hours.py +0 -0
  184. {thth-4.5.0 → thth-4.5.2}/tests/test_r2_r4_r5.py +0 -0
  185. {thth-4.5.0 → thth-4.5.2}/tests/test_read_cli_bare_runtimeerror.py +0 -0
  186. {thth-4.5.0 → thth-4.5.2}/tests/test_reason_adoption.py +0 -0
  187. {thth-4.5.0 → thth-4.5.2}/tests/test_redact.py +0 -0
  188. {thth-4.5.0 → thth-4.5.2}/tests/test_redirect_token_leak.py +0 -0
  189. {thth-4.5.0 → thth-4.5.2}/tests/test_refresh.py +0 -0
  190. {thth-4.5.0 → thth-4.5.2}/tests/test_refresh_destination.py +0 -0
  191. {thth-4.5.0 → thth-4.5.2}/tests/test_rejudge_new.py +0 -0
  192. {thth-4.5.0 → thth-4.5.2}/tests/test_rejudge_r2_r3.py +0 -0
  193. {thth-4.5.0 → thth-4.5.2}/tests/test_replies.py +0 -0
  194. {thth-4.5.0 → thth-4.5.2}/tests/test_replies_refresh.py +0 -0
  195. {thth-4.5.0 → thth-4.5.2}/tests/test_repo_sync.py +0 -0
  196. {thth-4.5.0 → thth-4.5.2}/tests/test_report_checkout_import.py +0 -0
  197. {thth-4.5.0 → thth-4.5.2}/tests/test_report_http.py +0 -0
  198. {thth-4.5.0 → thth-4.5.2}/tests/test_report_service.py +0 -0
  199. {thth-4.5.0 → thth-4.5.2}/tests/test_retract_already_gone.py +0 -0
  200. {thth-4.5.0 → thth-4.5.2}/tests/test_reverse_audit.py +0 -0
  201. {thth-4.5.0 → thth-4.5.2}/tests/test_review_case_id_cluster_m2.py +0 -0
  202. {thth-4.5.0 → thth-4.5.2}/tests/test_review_case_id_cluster_r2.py +0 -0
  203. {thth-4.5.0 → thth-4.5.2}/tests/test_review_cli.py +0 -0
  204. {thth-4.5.0 → thth-4.5.2}/tests/test_review_evidence_account.py +0 -0
  205. {thth-4.5.0 → thth-4.5.2}/tests/test_review_form_spec_and_case_id.py +0 -0
  206. {thth-4.5.0 → thth-4.5.2}/tests/test_review_provenance.py +0 -0
  207. {thth-4.5.0 → thth-4.5.2}/tests/test_review_records.py +0 -0
  208. {thth-4.5.0 → thth-4.5.2}/tests/test_revoke.py +0 -0
  209. {thth-4.5.0 → thth-4.5.2}/tests/test_roundtrip.py +0 -0
  210. {thth-4.5.0 → thth-4.5.2}/tests/test_run_missing_env.py +0 -0
  211. {thth-4.5.0 → thth-4.5.2}/tests/test_run_record_failclosed.py +0 -0
  212. {thth-4.5.0 → thth-4.5.2}/tests/test_runs_collected.py +0 -0
  213. {thth-4.5.0 → thth-4.5.2}/tests/test_runs_read_rows.py +0 -0
  214. {thth-4.5.0 → thth-4.5.2}/tests/test_schedule_multiple.py +0 -0
  215. {thth-4.5.0 → thth-4.5.2}/tests/test_schedule_view.py +0 -0
  216. {thth-4.5.0 → thth-4.5.2}/tests/test_scopes_source_requested.py +0 -0
  217. {thth-4.5.0 → thth-4.5.2}/tests/test_search_engage_targets.py +0 -0
  218. {thth-4.5.0 → thth-4.5.2}/tests/test_search_media_routes.py +0 -0
  219. {thth-4.5.0 → thth-4.5.2}/tests/test_secret_value_redact.py +0 -0
  220. {thth-4.5.0 → thth-4.5.2}/tests/test_secrets_grep.py +0 -0
  221. {thth-4.5.0 → thth-4.5.2}/tests/test_security_small_fixes.py +0 -0
  222. {thth-4.5.0 → thth-4.5.2}/tests/test_select.py +0 -0
  223. {thth-4.5.0 → thth-4.5.2}/tests/test_selfupdate.py +0 -0
  224. {thth-4.5.0 → thth-4.5.2}/tests/test_send.py +0 -0
  225. {thth-4.5.0 → thth-4.5.2}/tests/test_send_cli.py +0 -0
  226. {thth-4.5.0 → thth-4.5.2}/tests/test_send_control_chars.py +0 -0
  227. {thth-4.5.0 → thth-4.5.2}/tests/test_send_guard_cli.py +0 -0
  228. {thth-4.5.0 → thth-4.5.2}/tests/test_send_media.py +0 -0
  229. {thth-4.5.0 → thth-4.5.2}/tests/test_send_visibility.py +0 -0
  230. {thth-4.5.0 → thth-4.5.2}/tests/test_sent_integrity.py +0 -0
  231. {thth-4.5.0 → thth-4.5.2}/tests/test_server_json.py +0 -0
  232. {thth-4.5.0 → thth-4.5.2}/tests/test_site.py +0 -0
  233. {thth-4.5.0 → thth-4.5.2}/tests/test_skill_doc.py +0 -0
  234. {thth-4.5.0 → thth-4.5.2}/tests/test_study_report.py +0 -0
  235. {thth-4.5.0 → thth-4.5.2}/tests/test_systemd.py +0 -0
  236. {thth-4.5.0 → thth-4.5.2}/tests/test_thread_publish.py +0 -0
  237. {thth-4.5.0 → thth-4.5.2}/tests/test_thread_read.py +0 -0
  238. {thth-4.5.0 → thth-4.5.2}/tests/test_threadrun.py +0 -0
  239. {thth-4.5.0 → thth-4.5.2}/tests/test_threads_read_permissions.py +0 -0
  240. {thth-4.5.0 → thth-4.5.2}/tests/test_threadshape.py +0 -0
  241. {thth-4.5.0 → thth-4.5.2}/tests/test_thth_root.py +0 -0
  242. {thth-4.5.0 → thth-4.5.2}/tests/test_token_exists.py +0 -0
  243. {thth-4.5.0 → thth-4.5.2}/tests/test_token_scopes_record.py +0 -0
  244. {thth-4.5.0 → thth-4.5.2}/tests/test_token_set.py +0 -0
  245. {thth-4.5.0 → thth-4.5.2}/tests/test_topic.py +0 -0
  246. {thth-4.5.0 → thth-4.5.2}/tests/test_topic_advice.py +0 -0
  247. {thth-4.5.0 → thth-4.5.2}/tests/test_topic_cli.py +0 -0
  248. {thth-4.5.0 → thth-4.5.2}/tests/test_topic_models.py +0 -0
  249. {thth-4.5.0 → thth-4.5.2}/tests/test_topic_review_fixes.py +0 -0
  250. {thth-4.5.0 → thth-4.5.2}/tests/test_topic_shelf_observers.py +0 -0
  251. {thth-4.5.0 → thth-4.5.2}/tests/test_topic_store.py +0 -0
  252. {thth-4.5.0 → thth-4.5.2}/tests/test_topics_record_concurrent.py +0 -0
  253. {thth-4.5.0 → thth-4.5.2}/tests/test_topics_tool.py +0 -0
  254. {thth-4.5.0 → thth-4.5.2}/tests/test_trial_frictions.py +0 -0
  255. {thth-4.5.0 → thth-4.5.2}/tests/test_v1015_path_semantics.py +0 -0
  256. {thth-4.5.0 → thth-4.5.2}/tests/test_v1015_take_what_we_can.py +0 -0
  257. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_admin_transaction.py +0 -0
  258. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_approve_account.py +0 -0
  259. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_json_contract.py +0 -0
  260. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_length.py +0 -0
  261. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_lock_wait.py +0 -0
  262. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_mastodon_scopes.py +0 -0
  263. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_mentions.py +0 -0
  264. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_post_urls.py +0 -0
  265. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_repo_paths.py +0 -0
  266. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_reposts.py +0 -0
  267. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_study_add.py +0 -0
  268. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_tool_version.py +0 -0
  269. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_unanswered.py +0 -0
  270. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_unanswered_scope.py +0 -0
  271. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_where_arguments.py +0 -0
  272. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_where_filters.py +0 -0
  273. {thth-4.5.0 → thth-4.5.2}/tests/test_v210_who_arguments.py +0 -0
  274. {thth-4.5.0 → thth-4.5.2}/tests/test_v211_auth_integration.py +0 -0
  275. {thth-4.5.0 → thth-4.5.2}/tests/test_v211_authflow.py +0 -0
  276. {thth-4.5.0 → thth-4.5.2}/tests/test_v211_bluesky_stdin.py +0 -0
  277. {thth-4.5.0 → thth-4.5.2}/tests/test_v211_doctor_binding.py +0 -0
  278. {thth-4.5.0 → thth-4.5.2}/tests/test_v211_mastodon_auth.py +0 -0
  279. {thth-4.5.0 → thth-4.5.2}/tests/test_v211_x_auth.py +0 -0
  280. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_ask_coordination.py +0 -0
  281. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_budget.py +0 -0
  282. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_budget_auth.py +0 -0
  283. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_budget_mcp.py +0 -0
  284. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_deletion.py +0 -0
  285. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_leave.py +0 -0
  286. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_leave_gate.py +0 -0
  287. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_leave_providers.py +0 -0
  288. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_leave_token_safety.py +0 -0
  289. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_mastodon_guide.py +0 -0
  290. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_mcp_compat.py +0 -0
  291. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_missing_token.py +0 -0
  292. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_relay.py +0 -0
  293. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_server_writes.py +0 -0
  294. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_token_parent.py +0 -0
  295. {thth-4.5.0 → thth-4.5.2}/tests/test_v212_write_storage.py +0 -0
  296. {thth-4.5.0 → thth-4.5.2}/tests/test_v2131_send_dry_run.py +0 -0
  297. {thth-4.5.0 → thth-4.5.2}/tests/test_v2131_threads_publish_wait.py +0 -0
  298. {thth-4.5.0 → thth-4.5.2}/tests/test_v2132_send_inflight_hint.py +0 -0
  299. {thth-4.5.0 → thth-4.5.2}/tests/test_v2132_threads_create_timeout.py +0 -0
  300. {thth-4.5.0 → thth-4.5.2}/tests/test_v2132_threads_create_unknown.py +0 -0
  301. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_acceptance_fixes.py +0 -0
  302. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_adts_asf.py +0 -0
  303. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_attachment_kinds.py +0 -0
  304. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_external.py +0 -0
  305. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_media.py +0 -0
  306. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_metadata.py +0 -0
  307. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_metadata_uri.py +0 -0
  308. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_metadata_video.py +0 -0
  309. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_record.py +0 -0
  310. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_record_video.py +0 -0
  311. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_video.py +0 -0
  312. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bluesky_webm_named.py +0 -0
  313. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_bmff_typed_metadata.py +0 -0
  314. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_budget_lock.py +0 -0
  315. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_capabilities.py +0 -0
  316. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_carryover_timeout.py +0 -0
  317. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_graphemes.py +0 -0
  318. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_auth_media.py +0 -0
  319. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_bmff_audio.py +0 -0
  320. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_flac.py +0 -0
  321. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_gif.py +0 -0
  322. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_link.py +0 -0
  323. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_media.py +0 -0
  324. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_mp3.py +0 -0
  325. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_ogg_opus.py +0 -0
  326. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_ogg_vorbis.py +0 -0
  327. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_poll.py +0 -0
  328. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_primary_size_boundary.py +0 -0
  329. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_quote.py +0 -0
  330. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_thumbnail.py +0 -0
  331. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_wav.py +0 -0
  332. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_webm_audio.py +0 -0
  333. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_mastodon_webm_video.py +0 -0
  334. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_media_cleanup.py +0 -0
  335. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_media_formats.py +0 -0
  336. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_media_foundation.py +0 -0
  337. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_media_prepared.py +0 -0
  338. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_media_relay.py +0 -0
  339. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_media_relay_refusals.py +0 -0
  340. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_media_upload.py +0 -0
  341. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_review_followups.py +0 -0
  342. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_stage810_audit.py +0 -0
  343. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_static_diagnostics.py +0 -0
  344. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_stop_observation.py +0 -0
  345. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_systemd_worker.py +0 -0
  346. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_ghost.py +0 -0
  347. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_gif.py +0 -0
  348. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_link.py +0 -0
  349. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_media.py +0 -0
  350. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_media_spoiler.py +0 -0
  351. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_poll.py +0 -0
  352. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_quote.py +0 -0
  353. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_quote_media.py +0 -0
  354. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_replies.py +0 -0
  355. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_text.py +0 -0
  356. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_text_spoiler.py +0 -0
  357. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_threads_video.py +0 -0
  358. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_transport_review.py +0 -0
  359. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_video_typed_metadata.py +0 -0
  360. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_vorbis_comment_boundary.py +0 -0
  361. {thth-4.5.0 → thth-4.5.2}/tests/test_v213_webm_schema_boundary.py +0 -0
  362. {thth-4.5.0 → thth-4.5.2}/tests/test_v2141_draft_lint_reason.py +0 -0
  363. {thth-4.5.0 → thth-4.5.2}/tests/test_v2141_managed_repo_umask.py +0 -0
  364. {thth-4.5.0 → thth-4.5.2}/tests/test_v214_release_signing.py +0 -0
  365. {thth-4.5.0 → thth-4.5.2}/tests/test_v214_x_adapter.py +0 -0
  366. {thth-4.5.0 → thth-4.5.2}/tests/test_v214_x_auth.py +0 -0
  367. {thth-4.5.0 → thth-4.5.2}/tests/test_v214_x_media.py +0 -0
  368. {thth-4.5.0 → thth-4.5.2}/tests/test_v214_x_posts_budget.py +0 -0
  369. {thth-4.5.0 → thth-4.5.2}/tests/test_v21b_approved_writes.py +0 -0
  370. {thth-4.5.0 → thth-4.5.2}/tests/test_v28_tags.py +0 -0
  371. {thth-4.5.0 → thth-4.5.2}/tests/test_v3100_docs.py +0 -0
  372. {thth-4.5.0 → thth-4.5.2}/tests/test_v3100_invite_admin_approval.py +0 -0
  373. {thth-4.5.0 → thth-4.5.2}/tests/test_v3100_invite_create.py +0 -0
  374. {thth-4.5.0 → thth-4.5.2}/tests/test_v3100_invite_credential.py +0 -0
  375. {thth-4.5.0 → thth-4.5.2}/tests/test_v3100_invite_limits.py +0 -0
  376. {thth-4.5.0 → thth-4.5.2}/tests/test_v3100_invite_worker.py +0 -0
  377. {thth-4.5.0 → thth-4.5.2}/tests/test_v310_morning.py +0 -0
  378. {thth-4.5.0 → thth-4.5.2}/tests/test_v310_search_timeout.py +0 -0
  379. {thth-4.5.0 → thth-4.5.2}/tests/test_v310_watch_words.py +0 -0
  380. {thth-4.5.0 → thth-4.5.2}/tests/test_v3111_observe_counts_only.py +0 -0
  381. {thth-4.5.0 → thth-4.5.2}/tests/test_v3111_observe_no_world.py +0 -0
  382. {thth-4.5.0 → thth-4.5.2}/tests/test_v311_leave_same_user.py +0 -0
  383. {thth-4.5.0 → thth-4.5.2}/tests/test_v311_mastodon_retract.py +0 -0
  384. {thth-4.5.0 → thth-4.5.2}/tests/test_v311_morning_overdue.py +0 -0
  385. {thth-4.5.0 → thth-4.5.2}/tests/test_v311_morning_own_exclusion.py +0 -0
  386. {thth-4.5.0 → thth-4.5.2}/tests/test_v311_morning_unanswered_window.py +0 -0
  387. {thth-4.5.0 → thth-4.5.2}/tests/test_v311_notifications_guidance.py +0 -0
  388. {thth-4.5.0 → thth-4.5.2}/tests/test_v311_replies_owned.py +0 -0
  389. {thth-4.5.0 → thth-4.5.2}/tests/test_v3120_activity.py +0 -0
  390. {thth-4.5.0 → thth-4.5.2}/tests/test_v3120_admin_write_reasons.py +0 -0
  391. {thth-4.5.0 → thth-4.5.2}/tests/test_v3120_guard.py +0 -0
  392. {thth-4.5.0 → thth-4.5.2}/tests/test_v3120_worker_version.py +0 -0
  393. {thth-4.5.0 → thth-4.5.2}/tests/test_v312_leave_shared_token.py +0 -0
  394. {thth-4.5.0 → thth-4.5.2}/tests/test_v312_own_handles_stopped.py +0 -0
  395. {thth-4.5.0 → thth-4.5.2}/tests/test_v312_report_admin.py +0 -0
  396. {thth-4.5.0 → thth-4.5.2}/tests/test_v312_report_channel.py +0 -0
  397. {thth-4.5.0 → thth-4.5.2}/tests/test_v312_report_inbox.py +0 -0
  398. {thth-4.5.0 → thth-4.5.2}/tests/test_v312_report_mcp_user.py +0 -0
  399. {thth-4.5.0 → thth-4.5.2}/tests/test_v3140_api_relay.py +0 -0
  400. {thth-4.5.0 → thth-4.5.2}/tests/test_v3140_invite_key.py +0 -0
  401. {thth-4.5.0 → thth-4.5.2}/tests/test_v3140_remote_cli.py +0 -0
  402. {thth-4.5.0 → thth-4.5.2}/tests/test_v3140_server_reads.py +0 -0
  403. {thth-4.5.0 → thth-4.5.2}/tests/test_v3142_browser_login.py +0 -0
  404. {thth-4.5.0 → thth-4.5.2}/tests/test_v3143_activity_push.py +0 -0
  405. {thth-4.5.0 → thth-4.5.2}/tests/test_v3143_cli_umask_leave.py +0 -0
  406. {thth-4.5.0 → thth-4.5.2}/tests/test_v3143_inflight_remote.py +0 -0
  407. {thth-4.5.0 → thth-4.5.2}/tests/test_v3143_managed_repo_recover.py +0 -0
  408. {thth-4.5.0 → thth-4.5.2}/tests/test_v315_reactions.py +0 -0
  409. {thth-4.5.0 → thth-4.5.2}/tests/test_v317_run_lock_wait.py +0 -0
  410. {thth-4.5.0 → thth-4.5.2}/tests/test_v3180_json_names.py +0 -0
  411. {thth-4.5.0 → thth-4.5.2}/tests/test_v3180_map_timer.py +0 -0
  412. {thth-4.5.0 → thth-4.5.2}/tests/test_v3180_where_filters.py +0 -0
  413. {thth-4.5.0 → thth-4.5.2}/tests/test_v3180_where_keep.py +0 -0
  414. {thth-4.5.0 → thth-4.5.2}/tests/test_v3192_publish_hold.py +0 -0
  415. {thth-4.5.0 → thth-4.5.2}/tests/test_v31_analysis.py +0 -0
  416. {thth-4.5.0 → thth-4.5.2}/tests/test_v31_collection.py +0 -0
  417. {thth-4.5.0 → thth-4.5.2}/tests/test_v3200_x_weighted_length.py +0 -0
  418. {thth-4.5.0 → thth-4.5.2}/tests/test_v320_reply_to_file_display.py +0 -0
  419. {thth-4.5.0 → thth-4.5.2}/tests/test_v320_reply_to_file_lint.py +0 -0
  420. {thth-4.5.0 → thth-4.5.2}/tests/test_v320_reply_to_file_resolve.py +0 -0
  421. {thth-4.5.0 → thth-4.5.2}/tests/test_v320_report_fixes.py +0 -0
  422. {thth-4.5.0 → thth-4.5.2}/tests/test_v3220_account_scheduled.py +0 -0
  423. {thth-4.5.0 → thth-4.5.2}/tests/test_v3220_bluesky_reply_control.py +0 -0
  424. {thth-4.5.0 → thth-4.5.2}/tests/test_v3220_bluesky_retract.py +0 -0
  425. {thth-4.5.0 → thth-4.5.2}/tests/test_v3220_invite_mastodon.py +0 -0
  426. {thth-4.5.0 → thth-4.5.2}/tests/test_v3220_mentions_inbox.py +0 -0
  427. {thth-4.5.0 → thth-4.5.2}/tests/test_v3250_burst_reply_queue.py +0 -0
  428. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_fingerprint_version.py +0 -0
  429. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_held.py +0 -0
  430. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_held_names.py +0 -0
  431. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_last_refusals.py +0 -0
  432. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_notification_route.py +0 -0
  433. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_observe.py +0 -0
  434. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_report_friction.py +0 -0
  435. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_report_words.py +0 -0
  436. {thth-4.5.0 → thth-4.5.2}/tests/test_v330_reports_effect.py +0 -0
  437. {thth-4.5.0 → thth-4.5.2}/tests/test_v331_inflight_cli.py +0 -0
  438. {thth-4.5.0 → thth-4.5.2}/tests/test_v331_listing_resolve.py +0 -0
  439. {thth-4.5.0 → thth-4.5.2}/tests/test_v331_outbox_retention.py +0 -0
  440. {thth-4.5.0 → thth-4.5.2}/tests/test_v331_publish_status.py +0 -0
  441. {thth-4.5.0 → thth-4.5.2}/tests/test_v331_self_resolve.py +0 -0
  442. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_cli.py +0 -0
  443. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_docs.py +0 -0
  444. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_fields.py +0 -0
  445. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_mcp.py +0 -0
  446. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_morning.py +0 -0
  447. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_observe.py +0 -0
  448. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_open.py +0 -0
  449. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_open_confirm.py +0 -0
  450. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_reply.py +0 -0
  451. {thth-4.5.0 → thth-4.5.2}/tests/test_v340_plaza_store.py +0 -0
  452. {thth-4.5.0 → thth-4.5.2}/tests/test_v350_map_docs.py +0 -0
  453. {thth-4.5.0 → thth-4.5.2}/tests/test_v350_map_nodes.py +0 -0
  454. {thth-4.5.0 → thth-4.5.2}/tests/test_v350_map_observe.py +0 -0
  455. {thth-4.5.0 → thth-4.5.2}/tests/test_v350_map_plaza.py +0 -0
  456. {thth-4.5.0 → thth-4.5.2}/tests/test_v350_map_retention.py +0 -0
  457. {thth-4.5.0 → thth-4.5.2}/tests/test_v350_map_self.py +0 -0
  458. {thth-4.5.0 → thth-4.5.2}/tests/test_v350_map_show.py +0 -0
  459. {thth-4.5.0 → thth-4.5.2}/tests/test_v350_map_world.py +0 -0
  460. {thth-4.5.0 → thth-4.5.2}/tests/test_v351_leave_timers.py +0 -0
  461. {thth-4.5.0 → thth-4.5.2}/tests/test_v351_plaza_open_holes.py +0 -0
  462. {thth-4.5.0 → thth-4.5.2}/tests/test_v351_who_owned.py +0 -0
  463. {thth-4.5.0 → thth-4.5.2}/tests/test_v360_goal_analytics.py +0 -0
  464. {thth-4.5.0 → thth-4.5.2}/tests/test_v360_goal_docs.py +0 -0
  465. {thth-4.5.0 → thth-4.5.2}/tests/test_v360_goal_observe_map.py +0 -0
  466. {thth-4.5.0 → thth-4.5.2}/tests/test_v360_goal_plaza.py +0 -0
  467. {thth-4.5.0 → thth-4.5.2}/tests/test_v360_goal_record.py +0 -0
  468. {thth-4.5.0 → thth-4.5.2}/tests/test_v360_goal_send.py +0 -0
  469. {thth-4.5.0 → thth-4.5.2}/tests/test_v360_report_timeline.py +0 -0
  470. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_account_self_healing.py +0 -0
  471. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_approve_pending.py +0 -0
  472. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_click_attribution.py +0 -0
  473. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_collection_stale_basis.py +0 -0
  474. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_docs.py +0 -0
  475. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_follow_comparison.py +0 -0
  476. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_local_path.py +0 -0
  477. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_weekly_goals.py +0 -0
  478. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_where_aggregate.py +0 -0
  479. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_where_also.py +0 -0
  480. {thth-4.5.0 → thth-4.5.2}/tests/test_v370_where_zero.py +0 -0
  481. {thth-4.5.0 → thth-4.5.2}/tests/test_v380_docs.py +0 -0
  482. {thth-4.5.0 → thth-4.5.2}/tests/test_v380_plaza_digest.py +0 -0
  483. {thth-4.5.0 → thth-4.5.2}/tests/test_v380_plaza_from.py +0 -0
  484. {thth-4.5.0 → thth-4.5.2}/tests/test_v380_plaza_mcp.py +0 -0
  485. {thth-4.5.0 → thth-4.5.2}/tests/test_v380_plaza_moments.py +0 -0
  486. {thth-4.5.0 → thth-4.5.2}/tests/test_v380_plaza_owner.py +0 -0
  487. {thth-4.5.0 → thth-4.5.2}/tests/test_v380_plaza_received.py +0 -0
  488. {thth-4.5.0 → thth-4.5.2}/tests/test_v380_plaza_triggers.py +0 -0
  489. {thth-4.5.0 → thth-4.5.2}/tests/test_v381_plaza_visibility_default.py +0 -0
  490. {thth-4.5.0 → thth-4.5.2}/tests/test_v382_confirm_file.py +0 -0
  491. {thth-4.5.0 → thth-4.5.2}/tests/test_v382_first_stage_not_refusal.py +0 -0
  492. {thth-4.5.0 → thth-4.5.2}/tests/test_v382_reader_lock_and_push_pending.py +0 -0
  493. {thth-4.5.0 → thth-4.5.2}/tests/test_v382_sync_without_fetch_head.py +0 -0
  494. {thth-4.5.0 → thth-4.5.2}/tests/test_v390_docs.py +0 -0
  495. {thth-4.5.0 → thth-4.5.2}/tests/test_v390_per_post_clicks.py +0 -0
  496. {thth-4.5.0 → thth-4.5.2}/tests/test_v390_where_rate.py +0 -0
  497. {thth-4.5.0 → thth-4.5.2}/tests/test_v390_where_suggest.py +0 -0
  498. {thth-4.5.0 → thth-4.5.2}/tests/test_v391_plaza_text_numbers.py +0 -0
  499. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_jikki_1017.py +0 -0
  500. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_jikki_1017_bookmark.py +0 -0
  501. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_approve.py +0 -0
  502. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_deliveries.py +0 -0
  503. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_events.py +0 -0
  504. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_fingerprints.py +0 -0
  505. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_guard.py +0 -0
  506. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_locks.py +0 -0
  507. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_principals.py +0 -0
  508. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_publish.py +0 -0
  509. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s1_remote.py +0 -0
  510. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_adapter.py +0 -0
  511. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_audit.py +0 -0
  512. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_audit_fixes.py +0 -0
  513. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_commit.py +0 -0
  514. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_convert.py +0 -0
  515. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_held.py +0 -0
  516. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_oauth1.py +0 -0
  517. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_principal_auth.py +0 -0
  518. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s2_syntax_readback.py +0 -0
  519. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s3_adapter.py +0 -0
  520. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s3_convert.py +0 -0
  521. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s3_flow.py +0 -0
  522. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s4_announce.py +0 -0
  523. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s4_audit_fixes.py +0 -0
  524. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s4_bookmark.py +0 -0
  525. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s4_ledger.py +0 -0
  526. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s5_audit_fixes.py +0 -0
  527. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s5_conflict.py +0 -0
  528. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s5_detect.py +0 -0
  529. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s5_guard_remote.py +0 -0
  530. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s5_jikki.py +0 -0
  531. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s6_audit_fixes.py +0 -0
  532. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s6_bundle.py +0 -0
  533. {thth-4.5.0 → thth-4.5.2}/tests/test_v400_s6_collect.py +0 -0
  534. {thth-4.5.0 → thth-4.5.2}/tests/test_v401_board_hatena_token.py +0 -0
  535. {thth-4.5.0 → thth-4.5.2}/tests/test_v401_bookmark_burst.py +0 -0
  536. {thth-4.5.0 → thth-4.5.2}/tests/test_v410_audit_night.py +0 -0
  537. {thth-4.5.0 → thth-4.5.2}/tests/test_v410_bookmark_batch.py +0 -0
  538. {thth-4.5.0 → thth-4.5.2}/tests/test_v411_batch_cleanup.py +0 -0
  539. {thth-4.5.0 → thth-4.5.2}/tests/test_v420_bookmark_edit.py +0 -0
  540. {thth-4.5.0 → thth-4.5.2}/tests/test_v420_bookmark_tag_bytes.py +0 -0
  541. {thth-4.5.0 → thth-4.5.2}/tests/test_v421_audit_fixes.py +0 -0
  542. {thth-4.5.0 → thth-4.5.2}/tests/test_v422_bookmark_rejections.py +0 -0
  543. {thth-4.5.0 → thth-4.5.2}/tests/test_v422_sparse_checkout.py +0 -0
  544. {thth-4.5.0 → thth-4.5.2}/tests/test_v430_reply_batch.py +0 -0
  545. {thth-4.5.0 → thth-4.5.2}/tests/test_v431_stopped_timer.py +0 -0
  546. {thth-4.5.0 → thth-4.5.2}/tests/test_v440_reply_follow.py +0 -0
  547. {thth-4.5.0 → thth-4.5.2}/tests/test_v441_approval_ledger.py +0 -0
  548. {thth-4.5.0 → thth-4.5.2}/tests/test_v450_x_full_archive.py +0 -0
  549. {thth-4.5.0 → thth-4.5.2}/tests/test_value_domains_shown.py +0 -0
  550. {thth-4.5.0 → thth-4.5.2}/tests/test_version.py +0 -0
  551. {thth-4.5.0 → thth-4.5.2}/tests/test_where_cli.py +0 -0
  552. {thth-4.5.0 → thth-4.5.2}/tests/test_where_threads_tag_author.py +0 -0
  553. {thth-4.5.0 → thth-4.5.2}/tests/test_who_cli.py +0 -0
  554. {thth-4.5.0 → thth-4.5.2}/tests/test_writeback_rebase.py +0 -0
  555. {thth-4.5.0 → thth-4.5.2}/tests/test_writeback_validate.py +0 -0
  556. {thth-4.5.0 → thth-4.5.2}/tests/test_x_community.py +0 -0
  557. {thth-4.5.0 → thth-4.5.2}/tests/test_x_list.py +0 -0
  558. {thth-4.5.0 → thth-4.5.2}/tests/test_x_metrics.py +0 -0
  559. {thth-4.5.0 → thth-4.5.2}/tests/test_x_owned_metrics.py +0 -0
  560. {thth-4.5.0 → thth-4.5.2}/tests/test_x_search_thread.py +0 -0
  561. {thth-4.5.0 → thth-4.5.2}/tests/test_x_token_autorefresh.py +0 -0
  562. {thth-4.5.0 → thth-4.5.2}/tests/test_x_trends_counts.py +0 -0
  563. {thth-4.5.0 → thth-4.5.2}/thth/__init__.py +0 -0
  564. {thth-4.5.0 → thth-4.5.2}/thth/__main__.py +0 -0
  565. {thth-4.5.0 → thth-4.5.2}/thth/_grapheme_data.py +0 -0
  566. {thth-4.5.0 → thth-4.5.2}/thth/account_cli.py +0 -0
  567. {thth-4.5.0 → thth-4.5.2}/thth/account_report.py +0 -0
  568. {thth-4.5.0 → thth-4.5.2}/thth/account_settings.py +0 -0
  569. {thth-4.5.0 → thth-4.5.2}/thth/accounts.py +0 -0
  570. {thth-4.5.0 → thth-4.5.2}/thth/activity.py +0 -0
  571. {thth-4.5.0 → thth-4.5.2}/thth/adapters/__init__.py +0 -0
  572. {thth-4.5.0 → thth-4.5.2}/thth/adapters/_x_text_data.py +0 -0
  573. {thth-4.5.0 → thth-4.5.2}/thth/adapters/auth_mastodon.py +0 -0
  574. {thth-4.5.0 → thth-4.5.2}/thth/adapters/auth_threads.py +0 -0
  575. {thth-4.5.0 → thth-4.5.2}/thth/adapters/auth_x.py +0 -0
  576. {thth-4.5.0 → thth-4.5.2}/thth/adapters/base.py +0 -0
  577. {thth-4.5.0 → thth-4.5.2}/thth/adapters/bluesky.py +0 -0
  578. {thth-4.5.0 → thth-4.5.2}/thth/adapters/bluesky_media.py +0 -0
  579. {thth-4.5.0 → thth-4.5.2}/thth/adapters/bluesky_video.py +0 -0
  580. {thth-4.5.0 → thth-4.5.2}/thth/adapters/fotolife.py +0 -0
  581. {thth-4.5.0 → thth-4.5.2}/thth/adapters/hatena_common.py +0 -0
  582. {thth-4.5.0 → thth-4.5.2}/thth/adapters/hatenablog.py +0 -0
  583. {thth-4.5.0 → thth-4.5.2}/thth/adapters/hatenabookmark.py +0 -0
  584. {thth-4.5.0 → thth-4.5.2}/thth/adapters/hatenastar.py +0 -0
  585. {thth-4.5.0 → thth-4.5.2}/thth/adapters/mastodon.py +0 -0
  586. {thth-4.5.0 → thth-4.5.2}/thth/adapters/mastodon_media.py +0 -0
  587. {thth-4.5.0 → thth-4.5.2}/thth/adapters/threads.py +0 -0
  588. {thth-4.5.0 → thth-4.5.2}/thth/adapters/threads_media.py +0 -0
  589. {thth-4.5.0 → thth-4.5.2}/thth/adapters/x.py +0 -0
  590. {thth-4.5.0 → thth-4.5.2}/thth/adapters/x_media.py +0 -0
  591. {thth-4.5.0 → thth-4.5.2}/thth/adapters/x_text.py +0 -0
  592. {thth-4.5.0 → thth-4.5.2}/thth/admin_diagnose.py +0 -0
  593. {thth-4.5.0 → thth-4.5.2}/thth/admin_log.py +0 -0
  594. {thth-4.5.0 → thth-4.5.2}/thth/admin_notifications.py +0 -0
  595. {thth-4.5.0 → thth-4.5.2}/thth/admin_report.py +0 -0
  596. {thth-4.5.0 → thth-4.5.2}/thth/advise.py +0 -0
  597. {thth-4.5.0 → thth-4.5.2}/thth/after_cli.py +0 -0
  598. {thth-4.5.0 → thth-4.5.2}/thth/analytics_clicks.py +0 -0
  599. {thth-4.5.0 → thth-4.5.2}/thth/analytics_comparison.py +0 -0
  600. {thth-4.5.0 → thth-4.5.2}/thth/analytics_goals.py +0 -0
  601. {thth-4.5.0 → thth-4.5.2}/thth/analytics_report.py +0 -0
  602. {thth-4.5.0 → thth-4.5.2}/thth/analytics_shapes.py +0 -0
  603. {thth-4.5.0 → thth-4.5.2}/thth/analytics_threads.py +0 -0
  604. {thth-4.5.0 → thth-4.5.2}/thth/analytics_weekly.py +0 -0
  605. {thth-4.5.0 → thth-4.5.2}/thth/announce.py +0 -0
  606. {thth-4.5.0 → thth-4.5.2}/thth/api_diagnostic.py +0 -0
  607. {thth-4.5.0 → thth-4.5.2}/thth/api_requests.py +0 -0
  608. {thth-4.5.0 → thth-4.5.2}/thth/appconfig.py +0 -0
  609. {thth-4.5.0 → thth-4.5.2}/thth/appenv.py +0 -0
  610. {thth-4.5.0 → thth-4.5.2}/thth/approval.py +0 -0
  611. {thth-4.5.0 → thth-4.5.2}/thth/approval_ledger.py +0 -0
  612. {thth-4.5.0 → thth-4.5.2}/thth/approve_pending.py +0 -0
  613. {thth-4.5.0 → thth-4.5.2}/thth/archive_cli.py +0 -0
  614. {thth-4.5.0 → thth-4.5.2}/thth/article_cli.py +0 -0
  615. {thth-4.5.0 → thth-4.5.2}/thth/article_reactions.py +0 -0
  616. {thth-4.5.0 → thth-4.5.2}/thth/ask.py +0 -0
  617. {thth-4.5.0 → thth-4.5.2}/thth/ask_cli.py +0 -0
  618. {thth-4.5.0 → thth-4.5.2}/thth/assess.py +0 -0
  619. {thth-4.5.0 → thth-4.5.2}/thth/audioformats.py +0 -0
  620. {thth-4.5.0 → thth-4.5.2}/thth/authclients.py +0 -0
  621. {thth-4.5.0 → thth-4.5.2}/thth/authflow.py +0 -0
  622. {thth-4.5.0 → thth-4.5.2}/thth/authpassword.py +0 -0
  623. {thth-4.5.0 → thth-4.5.2}/thth/bluesky_metadata.py +0 -0
  624. {thth-4.5.0 → thth-4.5.2}/thth/bookmark_batch.py +0 -0
  625. {thth-4.5.0 → thth-4.5.2}/thth/bookmark_cli.py +0 -0
  626. {thth-4.5.0 → thth-4.5.2}/thth/budget_x.py +0 -0
  627. {thth-4.5.0 → thth-4.5.2}/thth/budget_x_posts.py +0 -0
  628. {thth-4.5.0 → thth-4.5.2}/thth/bundle.py +0 -0
  629. {thth-4.5.0 → thth-4.5.2}/thth/burst_queue.py +0 -0
  630. {thth-4.5.0 → thth-4.5.2}/thth/click_attribution.py +0 -0
  631. {thth-4.5.0 → thth-4.5.2}/thth/collect.py +0 -0
  632. {thth-4.5.0 → thth-4.5.2}/thth/collection_context.py +0 -0
  633. {thth-4.5.0 → thth-4.5.2}/thth/collection_status.py +0 -0
  634. {thth-4.5.0 → thth-4.5.2}/thth/community_cli.py +0 -0
  635. {thth-4.5.0 → thth-4.5.2}/thth/conflict_cli.py +0 -0
  636. {thth-4.5.0 → thth-4.5.2}/thth/core.py +0 -0
  637. {thth-4.5.0 → thth-4.5.2}/thth/deletion.py +0 -0
  638. {thth-4.5.0 → thth-4.5.2}/thth/deliveries.py +0 -0
  639. {thth-4.5.0 → thth-4.5.2}/thth/deliveries_cli.py +0 -0
  640. {thth-4.5.0 → thth-4.5.2}/thth/doctor.py +0 -0
  641. {thth-4.5.0 → thth-4.5.2}/thth/ebmlaudio.py +0 -0
  642. {thth-4.5.0 → thth-4.5.2}/thth/ebmlvideo.py +0 -0
  643. {thth-4.5.0 → thth-4.5.2}/thth/engagements.py +0 -0
  644. {thth-4.5.0 → thth-4.5.2}/thth/fingerprint_golden.py +0 -0
  645. {thth-4.5.0 → thth-4.5.2}/thth/flacformats.py +0 -0
  646. {thth-4.5.0 → thth-4.5.2}/thth/forms.py +0 -0
  647. {thth-4.5.0 → thth-4.5.2}/thth/fotolife_cli.py +0 -0
  648. {thth-4.5.0 → thth-4.5.2}/thth/goals.py +0 -0
  649. {thth-4.5.0 → thth-4.5.2}/thth/graphemes.py +0 -0
  650. {thth-4.5.0 → thth-4.5.2}/thth/guard.py +0 -0
  651. {thth-4.5.0 → thth-4.5.2}/thth/handoff_cursor.py +0 -0
  652. {thth-4.5.0 → thth-4.5.2}/thth/hatena_bookmark.py +0 -0
  653. {thth-4.5.0 → thth-4.5.2}/thth/hatena_cli.py +0 -0
  654. {thth-4.5.0 → thth-4.5.2}/thth/hatena_conflict.py +0 -0
  655. {thth-4.5.0 → thth-4.5.2}/thth/hatena_convert.py +0 -0
  656. {thth-4.5.0 → thth-4.5.2}/thth/hatena_entry.py +0 -0
  657. {thth-4.5.0 → thth-4.5.2}/thth/hatena_fotolife.py +0 -0
  658. {thth-4.5.0 → thth-4.5.2}/thth/hatena_publish.py +0 -0
  659. {thth-4.5.0 → thth-4.5.2}/thth/hatena_reactions.py +0 -0
  660. {thth-4.5.0 → thth-4.5.2}/thth/healthcheck.py +0 -0
  661. {thth-4.5.0 → thth-4.5.2}/thth/httpsafe.py +0 -0
  662. {thth-4.5.0 → thth-4.5.2}/thth/incident.py +0 -0
  663. {thth-4.5.0 → thth-4.5.2}/thth/inflight.py +0 -0
  664. {thth-4.5.0 → thth-4.5.2}/thth/inflight_cli.py +0 -0
  665. {thth-4.5.0 → thth-4.5.2}/thth/inflight_resolve.py +0 -0
  666. {thth-4.5.0 → thth-4.5.2}/thth/invites.py +0 -0
  667. {thth-4.5.0 → thth-4.5.2}/thth/jst.py +0 -0
  668. {thth-4.5.0 → thth-4.5.2}/thth/leave.py +0 -0
  669. {thth-4.5.0 → thth-4.5.2}/thth/leave_gate.py +0 -0
  670. {thth-4.5.0 → thth-4.5.2}/thth/limits_config.py +0 -0
  671. {thth-4.5.0 → thth-4.5.2}/thth/limits_policy.py +0 -0
  672. {thth-4.5.0 → thth-4.5.2}/thth/limits_view.py +0 -0
  673. {thth-4.5.0 → thth-4.5.2}/thth/lint.py +0 -0
  674. {thth-4.5.0 → thth-4.5.2}/thth/list_cli.py +0 -0
  675. {thth-4.5.0 → thth-4.5.2}/thth/lock.py +0 -0
  676. {thth-4.5.0 → thth-4.5.2}/thth/maintain.py +0 -0
  677. {thth-4.5.0 → thth-4.5.2}/thth/managed_repo.py +0 -0
  678. {thth-4.5.0 → thth-4.5.2}/thth/map_cli.py +0 -0
  679. {thth-4.5.0 → thth-4.5.2}/thth/map_store.py +0 -0
  680. {thth-4.5.0 → thth-4.5.2}/thth/map_view.py +0 -0
  681. {thth-4.5.0 → thth-4.5.2}/thth/map_world.py +0 -0
  682. {thth-4.5.0 → thth-4.5.2}/thth/measured.py +0 -0
  683. {thth-4.5.0 → thth-4.5.2}/thth/media.py +0 -0
  684. {thth-4.5.0 → thth-4.5.2}/thth/media_capabilities.py +0 -0
  685. {thth-4.5.0 → thth-4.5.2}/thth/media_cleanup.py +0 -0
  686. {thth-4.5.0 → thth-4.5.2}/thth/media_cli.py +0 -0
  687. {thth-4.5.0 → thth-4.5.2}/thth/media_delivery.py +0 -0
  688. {thth-4.5.0 → thth-4.5.2}/thth/media_observations.py +0 -0
  689. {thth-4.5.0 → thth-4.5.2}/thth/media_relay.py +0 -0
  690. {thth-4.5.0 → thth-4.5.2}/thth/media_traits/bluesky.json +0 -0
  691. {thth-4.5.0 → thth-4.5.2}/thth/media_traits/fotolife.json +0 -0
  692. {thth-4.5.0 → thth-4.5.2}/thth/media_traits/hatenablog.json +0 -0
  693. {thth-4.5.0 → thth-4.5.2}/thth/media_traits/hatenabookmark.json +0 -0
  694. {thth-4.5.0 → thth-4.5.2}/thth/media_traits/mastodon.json +0 -0
  695. {thth-4.5.0 → thth-4.5.2}/thth/media_traits/misskey.json +0 -0
  696. {thth-4.5.0 → thth-4.5.2}/thth/media_traits/threads.json +0 -0
  697. {thth-4.5.0 → thth-4.5.2}/thth/media_traits/x.json +0 -0
  698. {thth-4.5.0 → thth-4.5.2}/thth/media_traits.py +0 -0
  699. {thth-4.5.0 → thth-4.5.2}/thth/media_uploads.py +0 -0
  700. {thth-4.5.0 → thth-4.5.2}/thth/mediaformats.py +0 -0
  701. {thth-4.5.0 → thth-4.5.2}/thth/morning.py +0 -0
  702. {thth-4.5.0 → thth-4.5.2}/thth/mpeg_audio.py +0 -0
  703. {thth-4.5.0 → thth-4.5.2}/thth/notification_route.py +0 -0
  704. {thth-4.5.0 → thth-4.5.2}/thth/oauth.py +0 -0
  705. {thth-4.5.0 → thth-4.5.2}/thth/oauth1.py +0 -0
  706. {thth-4.5.0 → thth-4.5.2}/thth/oggformats.py +0 -0
  707. {thth-4.5.0 → thth-4.5.2}/thth/operations_handoff.py +0 -0
  708. {thth-4.5.0 → thth-4.5.2}/thth/plaza.py +0 -0
  709. {thth-4.5.0 → thth-4.5.2}/thth/plaza_cli.py +0 -0
  710. {thth-4.5.0 → thth-4.5.2}/thth/plaza_from.py +0 -0
  711. {thth-4.5.0 → thth-4.5.2}/thth/plaza_moments.py +0 -0
  712. {thth-4.5.0 → thth-4.5.2}/thth/plaza_observe.py +0 -0
  713. {thth-4.5.0 → thth-4.5.2}/thth/plaza_reads.py +0 -0
  714. {thth-4.5.0 → thth-4.5.2}/thth/plaza_redact.py +0 -0
  715. {thth-4.5.0 → thth-4.5.2}/thth/postid.py +0 -0
  716. {thth-4.5.0 → thth-4.5.2}/thth/principal_cli.py +0 -0
  717. {thth-4.5.0 → thth-4.5.2}/thth/principals.py +0 -0
  718. {thth-4.5.0 → thth-4.5.2}/thth/private_store.py +0 -0
  719. {thth-4.5.0 → thth-4.5.2}/thth/publish_hold.py +0 -0
  720. {thth-4.5.0 → thth-4.5.2}/thth/pulse.py +0 -0
  721. {thth-4.5.0 → thth-4.5.2}/thth/queuefile.py +0 -0
  722. {thth-4.5.0 → thth-4.5.2}/thth/reaction_cli.py +0 -0
  723. {thth-4.5.0 → thth-4.5.2}/thth/reactions.py +0 -0
  724. {thth-4.5.0 → thth-4.5.2}/thth/read_coordination.py +0 -0
  725. {thth-4.5.0 → thth-4.5.2}/thth/read_window.py +0 -0
  726. {thth-4.5.0 → thth-4.5.2}/thth/redact.py +0 -0
  727. {thth-4.5.0 → thth-4.5.2}/thth/refusals.py +0 -0
  728. {thth-4.5.0 → thth-4.5.2}/thth/relay.py +0 -0
  729. {thth-4.5.0 → thth-4.5.2}/thth/remote.py +0 -0
  730. {thth-4.5.0 → thth-4.5.2}/thth/replies.py +0 -0
  731. {thth-4.5.0 → thth-4.5.2}/thth/reply_follow.py +0 -0
  732. {thth-4.5.0 → thth-4.5.2}/thth/report.py +0 -0
  733. {thth-4.5.0 → thth-4.5.2}/thth/report_details.py +0 -0
  734. {thth-4.5.0 → thth-4.5.2}/thth/report_http.py +0 -0
  735. {thth-4.5.0 → thth-4.5.2}/thth/report_inbox.py +0 -0
  736. {thth-4.5.0 → thth-4.5.2}/thth/report_service.py +0 -0
  737. {thth-4.5.0 → thth-4.5.2}/thth/report_timeline.py +0 -0
  738. {thth-4.5.0 → thth-4.5.2}/thth/retract_cli.py +0 -0
  739. {thth-4.5.0 → thth-4.5.2}/thth/runs.py +0 -0
  740. {thth-4.5.0 → thth-4.5.2}/thth/scopes.py +0 -0
  741. {thth-4.5.0 → thth-4.5.2}/thth/secrets_fs.py +0 -0
  742. {thth-4.5.0 → thth-4.5.2}/thth/select.py +0 -0
  743. {thth-4.5.0 → thth-4.5.2}/thth/selfupdate.py +0 -0
  744. {thth-4.5.0 → thth-4.5.2}/thth/sent.py +0 -0
  745. {thth-4.5.0 → thth-4.5.2}/thth/server_files.py +0 -0
  746. {thth-4.5.0 → thth-4.5.2}/thth/server_reads.py +0 -0
  747. {thth-4.5.0 → thth-4.5.2}/thth/server_writes.py +0 -0
  748. {thth-4.5.0 → thth-4.5.2}/thth/stop_observation.py +0 -0
  749. {thth-4.5.0 → thth-4.5.2}/thth/study_cli.py +0 -0
  750. {thth-4.5.0 → thth-4.5.2}/thth/study_report.py +0 -0
  751. {thth-4.5.0 → thth-4.5.2}/thth/summary_cli.py +0 -0
  752. {thth-4.5.0 → thth-4.5.2}/thth/systemd_gen.py +0 -0
  753. {thth-4.5.0 → thth-4.5.2}/thth/tags.py +0 -0
  754. {thth-4.5.0 → thth-4.5.2}/thth/thread_read.py +0 -0
  755. {thth-4.5.0 → thth-4.5.2}/thth/threadrun.py +0 -0
  756. {thth-4.5.0 → thth-4.5.2}/thth/threads_read_cli.py +0 -0
  757. {thth-4.5.0 → thth-4.5.2}/thth/threadshape.py +0 -0
  758. {thth-4.5.0 → thth-4.5.2}/thth/threadthrow.py +0 -0
  759. {thth-4.5.0 → thth-4.5.2}/thth/timer_cleanup.py +0 -0
  760. {thth-4.5.0 → thth-4.5.2}/thth/tool_version.py +0 -0
  761. {thth-4.5.0 → thth-4.5.2}/thth/topic_advice.py +0 -0
  762. {thth-4.5.0 → thth-4.5.2}/thth/topic_cli.py +0 -0
  763. {thth-4.5.0 → thth-4.5.2}/thth/topic_models.py +0 -0
  764. {thth-4.5.0 → thth-4.5.2}/thth/topic_store.py +0 -0
  765. {thth-4.5.0 → thth-4.5.2}/thth/topics.py +0 -0
  766. {thth-4.5.0 → thth-4.5.2}/thth/trends_cli.py +0 -0
  767. {thth-4.5.0 → thth-4.5.2}/thth/unanswered.py +0 -0
  768. {thth-4.5.0 → thth-4.5.2}/thth/vorbisformats.py +0 -0
  769. {thth-4.5.0 → thth-4.5.2}/thth/watch_cli.py +0 -0
  770. {thth-4.5.0 → thth-4.5.2}/thth/where_cli.py +0 -0
  771. {thth-4.5.0 → thth-4.5.2}/thth/where_keep.py +0 -0
  772. {thth-4.5.0 → thth-4.5.2}/thth/where_suggest.py +0 -0
  773. {thth-4.5.0 → thth-4.5.2}/thth/who_cli.py +0 -0
  774. {thth-4.5.0 → thth-4.5.2}/thth/worker.py +0 -0
  775. {thth-4.5.0 → thth-4.5.2}/thth/worker_version.py +0 -0
  776. {thth-4.5.0 → thth-4.5.2}/thth/x_archive.py +0 -0
  777. {thth-4.5.0 → thth-4.5.2}/thth/x_metrics.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: thth
3
- Version: 4.5.0
3
+ Version: 4.5.2
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,6 +45,7 @@ English: [README.en.md](README.en.md) · サイト: https://thth.me · 文書の
45
45
  - **出す**: `thth send` で今すぐ、`thth schedule` で予約、`--reply-to` で返信です。
46
46
  - **ブックマークの連投は記事と別に数える**(4.0.1): はてなブックマークは主体の別の窓(既定 10 分に 10 件)で数え、超えても主体を止めずにその 1 件だけ断ります。1 日 30 件・同じホストへ 1 日 3 件(自分の記事は 1 日 1 件のまま)。記事の連投で止める作りは変わりません([リリースノート 4.0.1](docs/リリースノート_4.0.1_2026-10-17.md))。
47
47
  - **X の全期間の検索**(4.5.0): `thth archive <口座> --from <handle> --title <曲名> [--max-total 20] [--json]` で 2006 年からの他人の公開投稿を読みます(id・URL・時刻・本文・handle・保存しない)。`--count-only` は件数だけ(1 回 0.010 USD)。返った本数で月の X 予算に数え、1 日の本数は `x_archive_daily_reads`(既定 0=読まない)で別に縛ります。timer からは叩きません([リリースノート 4.5.0](docs/リリースノート_4.5.0_2026-10-21.md))。
48
+ - **push した直後の lint・approve**(4.5.1): パスが VM に無ければ、その repo を取り込んで 1 回だけ探し直します。取り込んでも無ければ VM と remote の commit を並べます([リリースノート 4.5.1](docs/リリースノート_4.5.1_2026-10-22.md))。
48
49
  - **承認は利用者の repo に commit を積まない**(4.4.1): `thth approve --confirm`・`thth revoke`・thth.me の予約と取り消しは、VM の承認の台帳(`state/<口座>/approvals.json`)に書き、repo の原稿は書き換えません(`thth approvals <口座>` で見られます)。口座の台帳を `require_gate: true` にすると、hub の承認の番号(`--approval A261021-25`)の無い確定を断ります([リリースノート 4.4.1](docs/リリースノート_4.4.1_2026-10-21.md))。
49
50
  - **会話の続く投稿は 20 分おきに返信を取り直す**(4.4.0): 投稿から 48 時間以内か、直近 48 時間に返信が付いた投稿は、刻みとは別に timer が会話を取り直します(1 回 10 本まで・刻みと数の記録は変えない・X は既定で外す・台帳の `reply_follow` で変えられる)([リリースノート 4.4.0](docs/リリースノート_4.4.0_2026-10-21.md))。
50
51
  - **止めた口座では timer は何も出さない**(4.3.1): 安全装置か持ち主が止めた口座では、予約の投稿・返信を queue に残し、`thth account resume` で戻せば次の timer で出ます([リリースノート 4.3.1](docs/リリースノート_4.3.1_2026-10-20.md))。
@@ -21,6 +21,7 @@ English: [README.en.md](README.en.md) · サイト: https://thth.me · 文書の
21
21
  - **出す**: `thth send` で今すぐ、`thth schedule` で予約、`--reply-to` で返信です。
22
22
  - **ブックマークの連投は記事と別に数える**(4.0.1): はてなブックマークは主体の別の窓(既定 10 分に 10 件)で数え、超えても主体を止めずにその 1 件だけ断ります。1 日 30 件・同じホストへ 1 日 3 件(自分の記事は 1 日 1 件のまま)。記事の連投で止める作りは変わりません([リリースノート 4.0.1](docs/リリースノート_4.0.1_2026-10-17.md))。
23
23
  - **X の全期間の検索**(4.5.0): `thth archive <口座> --from <handle> --title <曲名> [--max-total 20] [--json]` で 2006 年からの他人の公開投稿を読みます(id・URL・時刻・本文・handle・保存しない)。`--count-only` は件数だけ(1 回 0.010 USD)。返った本数で月の X 予算に数え、1 日の本数は `x_archive_daily_reads`(既定 0=読まない)で別に縛ります。timer からは叩きません([リリースノート 4.5.0](docs/リリースノート_4.5.0_2026-10-21.md))。
24
+ - **push した直後の lint・approve**(4.5.1): パスが VM に無ければ、その repo を取り込んで 1 回だけ探し直します。取り込んでも無ければ VM と remote の commit を並べます([リリースノート 4.5.1](docs/リリースノート_4.5.1_2026-10-22.md))。
24
25
  - **承認は利用者の repo に commit を積まない**(4.4.1): `thth approve --confirm`・`thth revoke`・thth.me の予約と取り消しは、VM の承認の台帳(`state/<口座>/approvals.json`)に書き、repo の原稿は書き換えません(`thth approvals <口座>` で見られます)。口座の台帳を `require_gate: true` にすると、hub の承認の番号(`--approval A261021-25`)の無い確定を断ります([リリースノート 4.4.1](docs/リリースノート_4.4.1_2026-10-21.md))。
25
26
  - **会話の続く投稿は 20 分おきに返信を取り直す**(4.4.0): 投稿から 48 時間以内か、直近 48 時間に返信が付いた投稿は、刻みとは別に timer が会話を取り直します(1 回 10 本まで・刻みと数の記録は変えない・X は既定で外す・台帳の `reply_follow` で変えられる)([リリースノート 4.4.0](docs/リリースノート_4.4.0_2026-10-21.md))。
26
27
  - **止めた口座では timer は何も出さない**(4.3.1): 安全装置か持ち主が止めた口座では、予約の投稿・返信を queue に残し、`thth account resume` で戻せば次の timer で出ます([リリースノート 4.3.1](docs/リリースノート_4.3.1_2026-10-20.md))。
@@ -10,7 +10,6 @@ from __future__ import annotations
10
10
  import os
11
11
  import subprocess
12
12
  import sys
13
- import time
14
13
 
15
14
  from tests.conftest import BIN_THTH, run_thth, write_queue_file
16
15
 
@@ -45,7 +44,7 @@ def test_9_ロック解放後は続けて投げられる(isolated_account):
45
44
  account_name = isolated_account["name"]
46
45
 
47
46
  holder = subprocess.Popen(
48
- [sys.executable, HOLD_LOCK, account_name, "1"],
47
+ [sys.executable, HOLD_LOCK, account_name, "60"],
49
48
  stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=dict(os.environ),
50
49
  )
51
50
  try:
@@ -54,9 +53,10 @@ def test_9_ロック解放後は続けて投げられる(isolated_account):
54
53
  blocked = run_thth(["throw", account_name])
55
54
  assert blocked.returncode == 1
56
55
  finally:
56
+ # 保持の秒数に頼らず手で解く(-n auto の負荷で throw の起動が 1 秒を越え、揺れた・10/22)。
57
+ holder.terminate()
57
58
  holder.wait(timeout=10)
58
59
 
59
60
  # holder が解放した後は、通常どおり dry-run で投げられる(exit 0・skip)。
60
- time.sleep(0.2)
61
61
  result = run_thth(["throw", account_name, "--json"])
62
62
  assert result.returncode == 0
@@ -64,7 +64,7 @@ def test_7_同じrepo_dirの2アカウント同時実行は後から来たほう
64
64
  isolated_account_factory(name="acct-b-threads", repo_dir=shared_repo)
65
65
 
66
66
  holder = subprocess.Popen(
67
- [sys.executable, HOLD_REPO_LOCK, shared_repo, "3"],
67
+ [sys.executable, HOLD_REPO_LOCK, shared_repo, "60"],
68
68
  stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=dict(os.environ),
69
69
  )
70
70
  try:
@@ -80,6 +80,7 @@ def test_7_同じrepo_dirの2アカウント同時実行は後から来たほう
80
80
  result_a = run_thth(["throw", acct_a["name"]])
81
81
  assert result_a.returncode == 1
82
82
  finally:
83
+ holder.terminate() # 保持の秒数に頼らない(負荷で 3 秒を越えて揺れた・10/22)
83
84
  holder.wait(timeout=10)
84
85
 
85
86
 
@@ -88,7 +89,7 @@ def test_repoロックが解放されれば続けて投げられる(isolated_acc
88
89
  isolated_account_factory(name="acct-c-threads", repo_dir=shared_repo)
89
90
 
90
91
  holder = subprocess.Popen(
91
- [sys.executable, HOLD_REPO_LOCK, shared_repo, "1"],
92
+ [sys.executable, HOLD_REPO_LOCK, shared_repo, "60"],
92
93
  stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=dict(os.environ),
93
94
  )
94
95
  try:
@@ -97,10 +98,11 @@ def test_repoロックが解放されれば続けて投げられる(isolated_acc
97
98
  blocked = run_thth(["throw", "acct-c-threads"])
98
99
  assert blocked.returncode == 1
99
100
  finally:
101
+ # 保持の秒数に頼らず手で解く(-n auto の負荷で throw の起動が 1 秒を越え、揺れた・10/22)。
102
+ # flock は保持者が終われば OS が外す。
103
+ holder.terminate()
100
104
  holder.wait(timeout=10)
101
105
 
102
- import time
103
- time.sleep(0.2)
104
106
  result = run_thth(["throw", "acct-c-threads", "--json"])
105
107
  assert result.returncode == 0
106
108
 
@@ -129,3 +129,21 @@ def test_root_parent_alias_preserves_in_root_symlink_detection(dedicated, tmp_pa
129
129
  (dedicated / "state").symlink_to(target)
130
130
  with pytest.raises(IsolationError, match="unsafe_report_tree"):
131
131
  validate_environment(str(root), {"demo": "project"})
132
+
133
+
134
+ # 揺れ(4.5.0 の publish run 39662720366): 木を歩く間に台帳の一時ファイルが os.replace で消えると、
135
+ # lstat の FileNotFoundError が invalid_report_environment になっていた。消えた子は読めないので見逃す。
136
+ def test_entry_vanishing_during_walk_is_tolerated(dedicated, monkeypatch):
137
+ data = dedicated / "state/demo"
138
+ data.mkdir(parents=True)
139
+ (data / "approvals.json").write_text("{}")
140
+ real_walk = os.walk
141
+
142
+ def racing_walk(path, *args, **kwargs):
143
+ for directory, dirs, files in real_walk(path, *args, **kwargs):
144
+ if Path(directory).resolve() == data.resolve():
145
+ files = [*files, "approvals.json.0f0f.tmp"] # 並べた直後に入れ替えで消えた
146
+ yield directory, dirs, files
147
+
148
+ monkeypatch.setattr(os, "walk", racing_walk)
149
+ validate_environment(str(dedicated), {"demo": "project"})
@@ -13,6 +13,11 @@ from tests.test_v212_server_writes import env,draft
13
13
  from tests.test_mcp import _load_server_module
14
14
 
15
15
 
16
+ # 「待たない」の上限。待つなら保持者が手放すまで(ここでは無期限に)止まるので、1 秒と 5 秒の差は
17
+ # 負荷の差でしかない(-n auto と他の試験が重なり 2.2 秒で揺れた・10/22)。
18
+ NO_WAIT=5
19
+
20
+
16
21
  @contextlib.contextmanager
17
22
  def exclusive(name='alpha',stopped=False):
18
23
  code="""import sys
@@ -59,7 +64,7 @@ def test_cli_reads_do_not_wait_or_call_core_when_leave_holds_exclusive(env,conte
59
64
  command=[x.replace('{queue}',str(content[0])).replace('{study}',str(content[1])) for x in command]
60
65
  with exclusive():
61
66
  start=time.monotonic();rc=cli.main(command);elapsed=time.monotonic()-start
62
- captured=capsys.readouterr();assert rc==2 and elapsed<1
67
+ captured=capsys.readouterr();assert rc==2 and elapsed<NO_WAIT
63
68
  assert json.loads(captured.out)=={'error':'account_leaving','cannot_say':['account_leaving']}
64
69
  assert __import__('tests.conftest').conftest.is_refusal(captured.err, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 1 行
65
70
 
@@ -68,7 +73,7 @@ def test_dry_send_input_then_nonwaiting_lease(env,tmp_path,capsys):
68
73
  text=tmp_path/'text';text.write_text('synthetic body')
69
74
  with exclusive():
70
75
  start=time.monotonic();rc=cli.main(['send','alpha','--text-file',str(text)])
71
- assert time.monotonic()-start<1 and rc==2
76
+ assert time.monotonic()-start<NO_WAIT and rc==2
72
77
  assert __import__('tests.conftest').conftest.is_refusal(capsys.readouterr().err, 'account_leaving') # 3.1.2 §3.5・3.3.0 B3: 断りの理由行の後ろに打てる報告の 1 行
73
78
  assert not (env['root']/'state/alpha/sent').exists()
74
79
 
@@ -108,7 +113,7 @@ def test_pure_reports_take_lease_and_preserve_nonempty_success(env,content,monke
108
113
  denied=call();assert denied.get('isError') and denied['content'][0]['text']=='account_leaving'
109
114
  else:
110
115
  with pytest.raises(service.ReportServiceError,match='^account_leaving$'):call()
111
- assert time.monotonic()-start<1
116
+ assert time.monotonic()-start<NO_WAIT
112
117
 
113
118
 
114
119
  def test_real_http_returns_specific_503_without_wait(env,content,monkeypatch):
@@ -121,7 +126,7 @@ def test_real_http_returns_specific_503_without_wait(env,content,monkeypatch):
121
126
  conn=http.client.HTTPConnection('127.0.0.1',server.server_port,timeout=2)
122
127
  start=time.monotonic();conn.request('POST','/report',json.dumps({'operation':'analytics_report','account':'alpha'}),{'Authorization':'Bearer '+env['bearer'],'Content-Type':'application/json'})
123
128
  response=conn.getresponse();payload=json.loads(response.read());conn.close()
124
- assert time.monotonic()-start<1 and response.status==503
129
+ assert time.monotonic()-start<NO_WAIT and response.status==503
125
130
  assert payload=={'error':'account_leaving','cannot_say':['account_leaving']}
126
131
  finally:server.shutdown();worker.join()
127
132
 
@@ -198,7 +203,7 @@ def test_read_lease_refuses_unsafe_coordination_file(env,kind,tmp_path,capsys):
198
203
  def test_all_stopped_project_busy_is_not_successful_zero(env,content,command,capsys):
199
204
  with exclusive(stopped=True):
200
205
  start=time.monotonic();rc=cli.main(command)
201
- assert time.monotonic()-start<1 and rc==2
206
+ assert time.monotonic()-start<NO_WAIT and rc==2
202
207
  assert json.loads(capsys.readouterr().out)['cannot_say']==['account_leaving']
203
208
 
204
209
 
@@ -162,7 +162,8 @@ def test_eventsは記録から並べ他人の本文を入れない(lived):
162
162
  assert payload["schema"] == "thth.events/1"
163
163
  kinds = [row["kind"] for row in payload["events"]]
164
164
  assert sorted(kinds) == sorted(["published", "retracted", "reply_received", "mention_received",
165
- "account_stopped", "publish_held", "inflight_open"])
165
+ "account_stopped", "publish_held", "inflight_open",
166
+ "schedule_low"]) # 4.5.2: 直近に公開していて承認済みの予約が無い口座
166
167
  for row in payload["events"]:
167
168
  assert {"schema", "kind", "account", "at", "occurred_at", "recorded_at", "ref", "summary",
168
169
  "event_id"} <= set(row)
@@ -206,7 +207,7 @@ def test_eventsのCLIとlimitと断り(lived, capsys, monkeypatch):
206
207
  monkeypatch.setattr(jst, "now_jst", lambda: NOW)
207
208
  assert cli.main(["events", ACCOUNT, "--since", "24h", "--limit", "2", "--json"]) == 0
208
209
  payload = json.loads(capsys.readouterr().out)
209
- assert payload["n"] == 2 and payload["n_total"] == 7
210
+ assert payload["n"] == 2 and payload["n_total"] == 8 # 4.5.2: schedule_low が 1 件増える
210
211
  assert cli.main(["events", ACCOUNT, "--since", "きのう", "--json"]) == 2
211
212
  assert json.loads(capsys.readouterr().out)["cannot_say"] == ["invalid_since"]
212
213
  assert cli.main(["events", "no-such", "--json"]) == 2
@@ -318,26 +319,30 @@ def test_eventsのカーソルは遅れて記録されたものを取りこぼ
318
319
  first = events_cli.build(ACCOUNT, since="7d", now=NOW)
319
320
  cursor = first["next_cursor"]
320
321
  assert cursor.startswith("c1.")
322
+ # 返信の流れを見る試験なので、始まりの時刻の無い続いている状態(schedule_low・毎回返る)は除いて数える。
323
+ def refs(payload):
324
+ return [r["ref"] for r in payload["events"] if r["kind"] != "schedule_low"]
325
+
321
326
  # 何も増えていなければ続きは空。
322
327
  empty = events_cli.build(ACCOUNT, after=cursor, now=NOW)
323
- assert empty["events"] == [] and empty["next_cursor"] == cursor
328
+ assert refs(empty) == [] and empty["next_cursor"] == cursor
324
329
  # 採取の 1 回の中で付いた時刻が、前に読んだ最後の時刻より少し前(遅れて書かれた)。
325
330
  t_last = max(jst.parse(r["recorded_at"]) for r in first["events"] if r["recorded_at"])
326
331
  _reply("R-LATE", written=jst.iso(t_last - datetime.timedelta(minutes=20)),
327
332
  collected=jst.iso(t_last - datetime.timedelta(minutes=5)))
328
333
  _reply("R-NEXT", written=jst.iso(NOW), collected=jst.iso(NOW + datetime.timedelta(minutes=1)))
329
334
  got = events_cli.build(ACCOUNT, after=cursor, now=NOW + datetime.timedelta(minutes=2))
330
- assert [r["ref"] for r in got["events"]] == ["R-LATE", "R-NEXT"]
335
+ assert refs(got) == ["R-LATE", "R-NEXT"]
331
336
  # 次のカーソルの続きには、もう出さない。
332
- assert events_cli.build(ACCOUNT, after=got["next_cursor"], now=NOW)["events"] == []
337
+ assert refs(events_cli.build(ACCOUNT, after=got["next_cursor"], now=NOW)) == []
333
338
  # limit で古い方から刻んでも、つないで読めば同じ。
334
339
  page = events_cli.build(ACCOUNT, after=cursor, limit=1, now=NOW)
335
340
  rest = events_cli.build(ACCOUNT, after=page["next_cursor"], now=NOW)
336
- assert [r["ref"] for r in page["events"] + rest["events"]] == ["R-LATE", "R-NEXT"]
341
+ assert [r["ref"] for r in page["events"] + rest["events"] if r["kind"] != "schedule_low"] == ["R-LATE", "R-NEXT"]
337
342
  # event_id からも続きを読める(今の流れの、記録した時刻の順でその後ろ。遅れて書かれた R-LATE の
338
343
  # 後ろには先に記録された R1 も並ぶ——取りこぼさない側。重なりは受け手が event_id で除く)。
339
- late_id = got["events"][0]["event_id"]
340
- assert [r["ref"] for r in events_cli.build(ACCOUNT, after=late_id, now=NOW)["events"]] == ["R1", "R-NEXT"]
344
+ late_id = [r for r in got["events"] if r["kind"] != "schedule_low"][0]["event_id"]
345
+ assert refs(events_cli.build(ACCOUNT, after=late_id, now=NOW)) == ["R1", "R-NEXT"]
341
346
 
342
347
 
343
348
  def test_eventsのカーソルの断り(lived):
@@ -0,0 +1,122 @@
1
+ """4.4.2: 手元と VM の食い違い(計画 4.4 ①・本線 第7 10/22 の返事で推しの範囲)。
2
+
3
+ - lint・approve は、パスが VM に無ければその口座の repo を取り込んで 1 回だけ探し直す
4
+ (push した直後に「VM の repo に無いパス」と断っていた)。
5
+ - 取り込んでも無ければ、VM と remote の commit を並べて push を確かめるよう言う。
6
+ - send --text-file に VM に無いパスが来たら、標準入力の道を言う。
7
+ - 書き戻しの push で rebase がぶつかったら、途中のまま残さない(kopicha #4254)。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ from tests.conftest import init_git_pair, run_git, run_thth, write_queue_file
14
+ from thth import writeback
15
+
16
+
17
+ def _push_new_draft(account, name):
18
+ seed = os.path.join(os.path.dirname(account["repo_dir"]), "seed")
19
+ path = write_queue_file(os.path.join(seed, "docs", "sns", "queue"), name,
20
+ body=f"## threads\n\n{name} の本文です。\n",
21
+ fm_overrides={"account": account["name"], "status": "draft",
22
+ "approved_sha": None})
23
+ # write_queue_file が seed で commit と push まで済ませる(手元で push した形)。
24
+ assert run_git(seed, ["status", "--porcelain"]).stdout.strip() == ""
25
+ return os.path.relpath(path, seed)
26
+
27
+
28
+ def test_approve_pulls_when_path_was_just_pushed(isolated_account):
29
+ rel = _push_new_draft(isolated_account, "fresh.md")
30
+ assert not os.path.exists(os.path.join(isolated_account["repo_dir"], rel))
31
+ first = run_thth(["approve", rel])
32
+ # 2 は「VM に無い」の断り。取り込んで見つかれば 1 段目(digest を見せて rc 1)まで進む。
33
+ assert first.returncode == 1, first.stderr
34
+ assert "digest: " in first.stdout
35
+ assert "取り込みました" in first.stderr
36
+
37
+
38
+ def test_lint_pulls_when_path_was_just_pushed(isolated_account):
39
+ rel = _push_new_draft(isolated_account, "lintme.md")
40
+ result = run_thth(["lint", rel])
41
+ assert result.returncode == 0, result.stderr
42
+ assert "取り込みました" in result.stderr
43
+
44
+
45
+ def test_missing_even_after_pull_names_both_commits(isolated_account):
46
+ result = run_thth(["approve", "docs/sns/queue/never-pushed.md"])
47
+ assert result.returncode == 2
48
+ assert "VM の repo に無いパスです" in result.stderr
49
+ assert "push は済んでいますか" in result.stderr
50
+
51
+
52
+ def test_send_text_file_refusal_offers_stdin(isolated_account):
53
+ result = run_thth(["send", isolated_account["name"], "--text-file", "/Users/someone/draft.txt"])
54
+ assert result.returncode == 2
55
+ assert "標準入力で渡せます" in result.stderr
56
+
57
+
58
+ def test_conflicting_rebase_is_aborted_not_left_in_progress(tmp_path):
59
+ pair = init_git_pair(tmp_path / "repo", seed_content="seed\n")
60
+ seed, work = pair["seed"], pair["work"]
61
+ with open(os.path.join(seed, "docs/sns/queue/a.md"), "w", encoding="utf-8") as stream:
62
+ stream.write("elsewhere\n")
63
+ run_git(seed, ["commit", "-q", "-am", "elsewhere"])
64
+ run_git(seed, ["push", "-q", "origin", "main"])
65
+ with open(os.path.join(pair["queue_dir"], "a.md"), "w", encoding="utf-8") as stream:
66
+ stream.write("written back by thth\n")
67
+
68
+ ok, err = writeback.commit_and_push(work, rel_path="docs/sns/queue/a.md", message="書き戻し")
69
+
70
+ assert not ok and "push に失敗しました" in err
71
+ for name in ("rebase-merge", "rebase-apply"):
72
+ marker = run_git(work, ["rev-parse", "--git-path", name]).stdout.strip()
73
+ assert not os.path.exists(os.path.join(work, marker)), name
74
+ # 自分の commit は残る(手で直せる)。
75
+ assert run_git(work, ["log", "-1", "--format=%s"]).stdout.strip() == "書き戻し"
76
+
77
+
78
+ # 監査 1: 前から rebase の途中の repo には commit しない(detached HEAD に積んで abort で消えていた)。
79
+ def test_preexisting_rebase_is_left_alone_and_nothing_is_lost(tmp_path):
80
+ pair = init_git_pair(tmp_path / "repo", seed_content="seed\n")
81
+ seed, work = pair["seed"], pair["work"]
82
+ with open(os.path.join(seed, "docs/sns/queue/a.md"), "w", encoding="utf-8") as stream:
83
+ stream.write("elsewhere\n")
84
+ run_git(seed, ["commit", "-q", "-am", "elsewhere"])
85
+ run_git(seed, ["push", "-q", "origin", "main"])
86
+ with open(os.path.join(pair["queue_dir"], "a.md"), "w", encoding="utf-8") as stream:
87
+ stream.write("local\n")
88
+ run_git(work, ["commit", "-q", "-am", "local"])
89
+ run_git(work, ["fetch", "-q", "origin"])
90
+ import subprocess
91
+ subprocess.run(["git", "-C", work, "rebase", "@{u}"], capture_output=True) # ぶつかって途中のまま
92
+ with open(os.path.join(pair["queue_dir"], "b.md"), "w", encoding="utf-8") as stream:
93
+ stream.write("written back\n")
94
+
95
+ ok, err = writeback.commit_and_push(work, rel_path="docs/sns/queue/b.md", message="書き戻し b")
96
+
97
+ assert not ok and "rebase の途中" in err
98
+ marker = run_git(work, ["rev-parse", "--git-path", "rebase-merge"]).stdout.strip()
99
+ assert os.path.exists(os.path.join(work, marker)) # 人の途中の rebase はそのまま
100
+ assert os.path.exists(os.path.join(pair["queue_dir"], "b.md")) # 書いたものは消えない
101
+
102
+
103
+ # 監査 2: 取り込んで HEAD が進んでも目当てが無ければ、commit を並べて push を確かめるよう言う。
104
+ def test_moved_but_still_missing_names_both_commits(isolated_account):
105
+ _push_new_draft(isolated_account, "other.md")
106
+ result = run_thth(["approve", "docs/sns/queue/not-this-one.md"])
107
+ assert result.returncode == 2
108
+ assert "取り込みました" in result.stderr and "push は済んでいますか" in result.stderr
109
+
110
+
111
+ # 監査 3: 相対パスは、そのフォルダを持たない口座の repo を取り込まない。
112
+ def test_relative_path_does_not_pull_unrelated_repo(isolated_account_factory, tmp_path):
113
+ from tests.conftest import init_real_repo
114
+ main = isolated_account_factory()
115
+ other_repo = init_real_repo(tmp_path, "unrelated")
116
+ isolated_account_factory(name="other-threads", repo_dir=other_repo)
117
+ _push_new_draft({"repo_dir": other_repo, "name": "other-threads"}, "x.md")
118
+ before = run_git(other_repo, ["rev-parse", "HEAD"]).stdout.strip()
119
+ result = run_thth(["lint", "nowhere/at/all.md"])
120
+ assert result.returncode == 2
121
+ assert run_git(other_repo, ["rev-parse", "HEAD"]).stdout.strip() == before
122
+ assert main["name"]
@@ -0,0 +1,84 @@
1
+ """4.5.2: 予約の在庫が 48 時間分を切ったら `thth events` に `schedule_low`(続いている状態)を出す。
2
+
3
+ 10/19〜22 にこぴちゃ(Bluesky・Mastodon)と関東の予約が尽きて投稿が止まった。人が気づいたのは
4
+ 止まったあとだった(ナイトシフト 10/22)。h2d は events を読むので、そこへ前もって出す。
5
+ 見るのは: 動いている口座だけ・承認済みで未来だけ数える・最後の予約が 48 時間より先なら出ない・
6
+ 読むだけで何も書かない。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import datetime
11
+ import os
12
+
13
+ from tests.conftest import write_queue_file
14
+ from thth import accounts as accounts_mod, events_cli, jst, sent
15
+
16
+
17
+ def _put(account, name, *, hours, status="approved"):
18
+ """今から `hours` 時間後に出る予約を 1 本置く(commit・push 済み)。"""
19
+ at = jst.now_jst() + datetime.timedelta(hours=hours)
20
+ write_queue_file(account["queue_dir"], name, body="## threads\n\n予約の本文です。\n",
21
+ fm_overrides={"account": account["name"], "status": status,
22
+ "publish_at": jst.iso(at)})
23
+
24
+
25
+ def _published(account, *, days_ago):
26
+ at = jst.now_jst() - datetime.timedelta(days=days_ago)
27
+ sent.write(accounts_mod.state_dir_for(account["name"]), post_id=f"p-{days_ago}", text="公開した本文",
28
+ body_hash="0" * 64, sent_at=jst.iso(at))
29
+
30
+
31
+ def _low(account):
32
+ payload = events_cli.build(account["name"], since="7d", now=jst.now_jst())
33
+ return [e for e in payload["events"] if e["kind"] == "schedule_low"], payload
34
+
35
+
36
+ def test_active_account_with_short_stock_is_flagged(isolated_account):
37
+ _published(isolated_account, days_ago=1)
38
+ _put(isolated_account, "a.md", hours=20)
39
+ rows, payload = _low(isolated_account)
40
+ assert len(rows) == 1
41
+ row = rows[0]
42
+ assert row["ongoing"] is True and row["ref"] == "stock"
43
+ assert row["floor_hours"] == 48 and 18 <= row["stock_hours"] <= 20
44
+ assert row["approved_future"] == 1 and row["last_publish_at"]
45
+ assert payload["by_kind"]["schedule_low"] == 1
46
+
47
+
48
+ def test_enough_stock_is_quiet(isolated_account):
49
+ _published(isolated_account, days_ago=1)
50
+ _put(isolated_account, "a.md", hours=20)
51
+ _put(isolated_account, "b.md", hours=72)
52
+ rows, _ = _low(isolated_account)
53
+ assert rows == []
54
+
55
+
56
+ def test_no_approved_future_at_all_is_flagged_with_zero_hours(isolated_account):
57
+ _published(isolated_account, days_ago=2)
58
+ rows, _ = _low(isolated_account)
59
+ assert len(rows) == 1 and rows[0]["stock_hours"] == 0 and "last_publish_at" not in rows[0]
60
+
61
+
62
+ def test_drafts_do_not_count_as_stock(isolated_account):
63
+ _published(isolated_account, days_ago=1)
64
+ _put(isolated_account, "draft.md", hours=200, status="draft")
65
+ rows, _ = _low(isolated_account)
66
+ assert len(rows) == 1 and rows[0]["approved_future"] == 0
67
+
68
+
69
+ def test_account_not_publishing_lately_is_not_flagged(isolated_account):
70
+ _published(isolated_account, days_ago=10) # 7 日より前
71
+ rows, _ = _low(isolated_account)
72
+ assert rows == []
73
+
74
+
75
+ def test_account_that_never_published_is_not_flagged(isolated_account):
76
+ rows, _ = _low(isolated_account)
77
+ assert rows == []
78
+
79
+
80
+ def test_reading_writes_nothing(isolated_account):
81
+ _published(isolated_account, days_ago=1)
82
+ before = sorted(os.listdir(isolated_account["queue_dir"]))
83
+ _low(isolated_account)
84
+ assert sorted(os.listdir(isolated_account["queue_dir"])) == before
@@ -0,0 +1 @@
1
+ 4.5.2
@@ -220,6 +220,10 @@ def _stale_clone_line(root: str, account_name: str) -> str | None:
220
220
  return unchecked or None
221
221
 
222
222
 
223
+ class AmbiguousPath(ValueError):
224
+ """同じパスが複数の repo にある(取り込んでも解けないので、探し直さない)。"""
225
+
226
+
223
227
  def _translate_local_path(path: str, purpose: str | None = None) -> str | None:
224
228
  """手元の絶対パスを VM のパスに読み替える。読み替えたら 1 行言う(無ければ None)。
225
229
 
@@ -229,7 +233,7 @@ def _translate_local_path(path: str, purpose: str | None = None) -> str | None:
229
233
  """
230
234
  found, missing = _local_path_candidates(path)
231
235
  if len(found) > 1:
232
- raise ValueError('複数の repo に同じパスがあります。VM の絶対パスで指定してください: '
236
+ raise AmbiguousPath('複数の repo に同じパスがあります。VM の絶対パスで指定してください: '
233
237
  + ' / '.join(sorted(found)))
234
238
  if not found:
235
239
  if missing:
@@ -237,7 +241,7 @@ def _translate_local_path(path: str, purpose: str | None = None) -> str | None:
237
241
  stale = _stale_clone_line(root, name)
238
242
  raise ValueError(f'手元のパスを VM の repo の {rel} に読み替えましたが、VM にありません'
239
243
  f'(repo: {root})。' + (stale + "。" if stale else "")
240
- + f'push してから thth pull {name} で取り込んでください')
244
+ + f'手元で push は済んでいますか(済んでいれば thth pull {name} で取り込めます)')
241
245
  return None
242
246
  candidate, (root, name) = next(iter(found.items()))
243
247
  print(f'手元のパスを VM のパスに読み替えました: {path} → {candidate}(repo: {root})',
@@ -282,11 +286,87 @@ def _resolve_repo_path(path: str, purpose: str | None = None) -> str:
282
286
  print(f'repo 相対パスを解決しました: {path} → {candidate}(repo: {root})', file=sys.stderr)
283
287
  return candidate
284
288
  if candidates:
285
- raise ValueError('複数の repo に同じパスがあります。絶対パスで指定してください: '
289
+ raise AmbiguousPath('複数の repo に同じパスがあります。絶対パスで指定してください: '
286
290
  + ' / '.join(sorted(candidates)))
287
291
  raise ValueError(f'VM の repo に無いパスです: {path}。thth queue <account> で置き場を確認してください')
288
292
 
289
293
 
294
+ def _repos_for_lookup(items, account=None) -> dict:
295
+ """見つからなかったパスを探す先の repo(`{realpath: account 名}`)。
296
+
297
+ `account` を渡されたらその口座の repo だけ。手元(Mac)の絶対パスは読み替えた先の
298
+ repo、VM の絶対パスはそれを含む repo。相対パスは、その親のフォルダが在るか、
299
+ 台帳の queue_dir で始まる repo だけ(打ち間違いで全口座の repo を動かさない)。
300
+ """
301
+ repos = {}
302
+ for name in accounts_mod.list_account_names():
303
+ if account is not None and name != account:
304
+ continue
305
+ try:
306
+ cfg = accounts_mod.load_account(name)
307
+ except (accounts_mod.AccountError, OSError, ValueError, TypeError, KeyError):
308
+ continue
309
+ repo = cfg.get('repo_dir')
310
+ if isinstance(repo, str) and os.path.isdir(repo):
311
+ queue_dir = cfg.get('queue_dir') if isinstance(cfg.get('queue_dir'), str) else ''
312
+ repos.setdefault(os.path.realpath(repo), (name, queue_dir.strip('/')))
313
+ if account is not None:
314
+ return {root: name for root, (name, _q) in repos.items()}
315
+ wanted = {}
316
+ for item in items:
317
+ if os.path.exists(item):
318
+ continue
319
+ if os.path.isabs(item):
320
+ _found, missing = _local_path_candidates(item)
321
+ for _rel, root, name in missing:
322
+ wanted.setdefault(root, name)
323
+ for root, (name, _q) in repos.items():
324
+ if os.path.abspath(item).startswith(root + os.sep):
325
+ wanted.setdefault(root, name)
326
+ continue
327
+ rel = os.path.normpath(item).replace(os.sep, '/')
328
+ for root, (name, queue_dir) in repos.items():
329
+ parent = os.path.dirname(os.path.join(root, rel))
330
+ if os.path.isdir(parent) or (queue_dir and rel.startswith(queue_dir + '/')):
331
+ wanted.setdefault(root, name)
332
+ return wanted
333
+
334
+
335
+ def _sync_for_lookup(items, account=None, wait=0) -> tuple:
336
+ """4.4.2: lint・approve でパスが VM に無いとき、探す先の repo を取り込む。
337
+
338
+ push した直後に「VM の repo に無いパス」と断っていた(取り込みは approve の中でも
339
+ パスを探したあとだった・計画 4.4 ①)。取り込みは `thth pull` と同じ
340
+ (repo ロックの下で `sync_repo`・早送りだけ)。ロックは `--wait` の秒数まで待つ。
341
+ 戻り値 `(1 つでも HEAD が進んだか, 探し直しても無いときに添える行の一覧)`。
342
+ """
343
+ moved, lines = False, []
344
+ for root, name in _repos_for_lookup(items, account).items():
345
+ before = writeback_mod._run_git(root, ["rev-parse", "--short", "HEAD"]).stdout.strip()
346
+ repo_lock = lock_mod.AccountLock(accounts_mod.repo_lock_path_for(root))
347
+ try:
348
+ lock_mod.acquire(repo_lock, wait)
349
+ except lock_mod.LockBusy:
350
+ lines.append(f"{name}: いまこの repo を別の実行が使っているので取り込めませんでした"
351
+ "(--wait <秒> で空くのを待てます)")
352
+ continue
353
+ try:
354
+ synced, sync_err, _sha = writeback_mod.sync_repo(root)
355
+ finally:
356
+ repo_lock.release()
357
+ if not synced:
358
+ lines.append(f"{name}: 取り込めませんでした: {sync_err}")
359
+ continue
360
+ after = writeback_mod._run_git(root, ["rev-parse", "--short", "HEAD"]).stdout.strip()
361
+ if after and after != before:
362
+ moved = True
363
+ print(f"{name}: パスが VM に無かったので取り込みました: {before} → {after}", file=sys.stderr)
364
+ upstream = writeback_mod._run_git(root, ["rev-parse", "--short", "@{u}"]).stdout.strip()
365
+ lines.append(f"{name}: 取り込んでも VM にありません(VM は {after}・remote は {upstream or '不明'})。"
366
+ "手元で commit と push は済んでいますか")
367
+ return moved, lines
368
+
369
+
290
370
  def _require_vm_path(path: str, *, what: str = "") -> str | None:
291
371
  """ファイルを受け取る引数に渡されたパスが VM に無いときの案内文を返す
292
372
  (T8-1・kopicha 続報)。存在すれば None。
@@ -316,7 +396,7 @@ def _require_vm_path(path: str, *, what: str = "") -> str | None:
316
396
  )
317
397
 
318
398
 
319
- def _expand_targets(files, *, only_draft: bool, account=None, purpose=None) -> tuple:
399
+ def _expand_targets(files, *, only_draft: bool, account=None, purpose=None, wait=0) -> tuple:
320
400
  """ファイルとディレクトリの混在を受けて、対象のファイル一覧に展開する。
321
401
 
322
402
  **ディレクトリを受けられるようにした**(kopicha セッション指摘 2026-09-10)。
@@ -339,8 +419,21 @@ def _expand_targets(files, *, only_draft: bool, account=None, purpose=None) -> t
339
419
  try:
340
420
  items = [_resolve_repo_path(item, purpose) for item in items]
341
421
  except ValueError as exc:
342
- print(str(exc), file=sys.stderr)
343
- return None, ''
422
+ # 4.4.2: lint・approve は、無いパスがあれば取り込んで 1 回だけ探し直す(計画 4.4 ①)。
423
+ moved, lines = (_sync_for_lookup(items, account, wait)
424
+ if purpose in ("lint", "approve") and not isinstance(exc, AmbiguousPath) else (False, []))
425
+ retried = None
426
+ if moved:
427
+ try:
428
+ retried = [_resolve_repo_path(item, purpose) for item in items]
429
+ except ValueError as again:
430
+ exc = again
431
+ if retried is None:
432
+ print(str(exc), file=sys.stderr)
433
+ for line in lines:
434
+ print(line, file=sys.stderr)
435
+ return None, ''
436
+ items = retried
344
437
  missing_msgs = []
345
438
  for item in items:
346
439
  if os.path.isdir(item):
@@ -671,7 +764,7 @@ def cmd_approve(args) -> int:
671
764
  " --confirm-file <file>(1 行に「<原稿のパス> <digest>」)。", file=sys.stderr)
672
765
  return 2
673
766
  paths, note = _expand_targets(args.file, only_draft=True, account=getattr(args, "account", None),
674
- purpose="approve")
767
+ purpose="approve", wait=getattr(args, "wait", 0))
675
768
  if paths is None:
676
769
  # **VM に無いパスは `_expand_targets` が案内済み**(T8-1)。
677
770
  return 2
@@ -3605,6 +3698,9 @@ def cmd_send(args) -> int:
3605
3698
  if vm_msg:
3606
3699
  # **VM に無いパスは素の traceback でなく案内で断る**(T8-1)。
3607
3700
  print(vm_msg, file=sys.stderr)
3701
+ # 4.4.2: 手元のファイルの本文なら標準入力で通る(MMR 1)。
3702
+ print(f" - 手元のファイルの本文なら標準入力で渡せます: thth send {args.account} … < <ファイル>",
3703
+ file=sys.stderr)
3608
3704
  return 2
3609
3705
  with open(args.text_file, encoding="utf-8") as f:
3610
3706
  text = f.read()