pyworklog 0.8.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. pyworklog-0.9.0/.dockerignore +11 -0
  2. {pyworklog-0.8.0 → pyworklog-0.9.0}/.github/workflows/test.yml +1 -1
  3. {pyworklog-0.8.0 → pyworklog-0.9.0}/AGENTS.md +1 -1
  4. {pyworklog-0.8.0 → pyworklog-0.9.0}/CHANGELOG.md +36 -1
  5. {pyworklog-0.8.0 → pyworklog-0.9.0}/DESIGN.md +24 -23
  6. {pyworklog-0.8.0 → pyworklog-0.9.0}/DESIGN.zh.md +23 -22
  7. pyworklog-0.9.0/Dockerfile.dev +33 -0
  8. {pyworklog-0.8.0 → pyworklog-0.9.0}/Makefile +19 -9
  9. {pyworklog-0.8.0 → pyworklog-0.9.0}/PKG-INFO +8 -8
  10. {pyworklog-0.8.0 → pyworklog-0.9.0}/README.md +7 -7
  11. {pyworklog-0.8.0 → pyworklog-0.9.0}/README.zh.md +7 -7
  12. {pyworklog-0.8.0 → pyworklog-0.9.0}/pyproject.toml +1 -1
  13. {pyworklog-0.8.0 → pyworklog-0.9.0}/skills/worklog-cli/SKILL.md +10 -8
  14. {pyworklog-0.8.0 → pyworklog-0.9.0}/skills/worklog-cli/integrations/statusline-wl.sh +2 -2
  15. {pyworklog-0.8.0 → pyworklog-0.9.0}/skills/worklog-cli/references/bulk.md +6 -4
  16. {pyworklog-0.8.0 → pyworklog-0.9.0}/skills/worklog-cli/references/features.md +8 -8
  17. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/cli.py +102 -42
  18. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/__init__.py +2 -1
  19. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/admin.py +1 -1
  20. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/bulk.py +72 -51
  21. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/checkin.py +13 -10
  22. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/goal.py +48 -13
  23. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/help.py +14 -14
  24. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/metric.py +31 -11
  25. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/query.py +317 -237
  26. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/semantic.py +190 -36
  27. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/state.py +364 -242
  28. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/timenodes.py +22 -12
  29. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/views.py +152 -145
  30. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/config.py +19 -0
  31. pyworklog-0.9.0/src/worklog/db.py +231 -0
  32. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/db_table.py +9 -3
  33. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/add.md +8 -5
  34. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/agent.md +1 -3
  35. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/area.md +4 -4
  36. pyworklog-0.9.0/src/worklog/help/en/body.md +24 -0
  37. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/checkin.md +1 -1
  38. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/goal.md +1 -1
  39. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/ids.md +1 -1
  40. pyworklog-0.9.0/src/worklog/help/en/import.md +21 -0
  41. pyworklog-0.9.0/src/worklog/help/en/kind.md +25 -0
  42. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/log.md +3 -2
  43. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/logs.md +1 -0
  44. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/ls.md +3 -2
  45. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/node.md +3 -3
  46. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/para.md +4 -4
  47. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/planning.md +1 -1
  48. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/project.md +1 -1
  49. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/projects.md +1 -1
  50. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/recap.md +5 -0
  51. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/relation.md +1 -0
  52. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/shortcuts.md +1 -0
  53. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/task.md +2 -1
  54. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/tracking.md +1 -1
  55. pyworklog-0.9.0/src/worklog/help/en/types.md +19 -0
  56. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/helpers.py +6 -6
  57. pyworklog-0.9.0/src/worklog/migrations/0011_drop_kind_column.py +54 -0
  58. pyworklog-0.9.0/src/worklog/node.py +50 -0
  59. pyworklog-0.9.0/src/worklog/node_schema.py +96 -0
  60. pyworklog-0.9.0/src/worklog/node_type_backfill.py +157 -0
  61. pyworklog-0.9.0/src/worklog/node_types.py +263 -0
  62. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/queries.py +175 -26
  63. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/render.py +52 -12
  64. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/templates/config.ini +7 -0
  65. pyworklog-0.9.0/src/worklog/timemodel.py +87 -0
  66. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/vectorstore.py +31 -10
  67. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/conftest.py +10 -0
  68. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_add.py +85 -52
  69. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_agent.py +50 -27
  70. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_brief.py +26 -26
  71. pyworklog-0.9.0/tests/test_canceled.py +90 -0
  72. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_cascade.py +7 -7
  73. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_changes.py +4 -4
  74. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_checkin.py +58 -28
  75. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_clock.py +32 -32
  76. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_completion.py +9 -0
  77. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_config.py +41 -0
  78. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_cycle_safety.py +8 -8
  79. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_dateinfo.py +2 -2
  80. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_day.py +63 -41
  81. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_db_table.py +19 -19
  82. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_default_verb.py +28 -28
  83. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_embedding.py +39 -0
  84. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_embedding_config.py +27 -0
  85. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_filters.py +62 -32
  86. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_find.py +75 -9
  87. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_focus.py +19 -19
  88. pyworklog-0.8.0/tests/test_meta.py → pyworklog-0.9.0/tests/test_goal.py +82 -22
  89. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_help.py +180 -8
  90. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_import_apply.py +129 -60
  91. pyworklog-0.9.0/tests/test_internals.py +141 -0
  92. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_link_set.py +42 -13
  93. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_log.py +156 -22
  94. pyworklog-0.9.0/tests/test_logs.py +419 -0
  95. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_ls.py +123 -51
  96. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_metric.py +88 -40
  97. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_migrations.py +178 -19
  98. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_node.py +49 -28
  99. pyworklog-0.9.0/tests/test_node_type_backfill.py +274 -0
  100. pyworklog-0.9.0/tests/test_node_types.py +209 -0
  101. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_plan.py +50 -47
  102. pyworklog-0.9.0/tests/test_projects.py +170 -0
  103. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_prop_clock.py +63 -15
  104. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_query.py +187 -23
  105. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_relation.py +51 -19
  106. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_render.py +15 -10
  107. pyworklog-0.9.0/tests/test_reserved_props.py +151 -0
  108. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_sample.py +8 -8
  109. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_sched.py +74 -57
  110. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_show.py +48 -21
  111. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_state.py +65 -18
  112. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_summary.py +12 -12
  113. pyworklog-0.9.0/tests/test_timemodel.py +151 -0
  114. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_tree.py +74 -74
  115. pyworklog-0.9.0/tests/test_type_props_create.py +249 -0
  116. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_vectorstore.py +31 -10
  117. {pyworklog-0.8.0 → pyworklog-0.9.0}/uv.lock +1 -1
  118. pyworklog-0.8.0/src/worklog/db.py +0 -136
  119. pyworklog-0.8.0/src/worklog/help/en/import.md +0 -18
  120. pyworklog-0.8.0/src/worklog/help/en/kind.md +0 -16
  121. pyworklog-0.8.0/src/worklog/help/en/kinds.md +0 -15
  122. pyworklog-0.8.0/tests/test_log_format.py +0 -166
  123. pyworklog-0.8.0/tests/test_logs.py +0 -136
  124. pyworklog-0.8.0/tests/test_projects.py +0 -122
  125. pyworklog-0.8.0/tests/test_ux.py +0 -862
  126. {pyworklog-0.8.0 → pyworklog-0.9.0}/.github/workflows/release.yml +0 -0
  127. {pyworklog-0.8.0 → pyworklog-0.9.0}/.gitignore +0 -0
  128. {pyworklog-0.8.0 → pyworklog-0.9.0}/CONTRIBUTING.md +0 -0
  129. {pyworklog-0.8.0 → pyworklog-0.9.0}/LICENSE +0 -0
  130. {pyworklog-0.8.0 → pyworklog-0.9.0}/pytest.ini +0 -0
  131. {pyworklog-0.8.0 → pyworklog-0.9.0}/skills/worklog-cli/integrations/wl-session-context.sh +0 -0
  132. {pyworklog-0.8.0 → pyworklog-0.9.0}/skills/worklog-cli/references/setup.md +0 -0
  133. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/__init__.py +0 -0
  134. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/alias.py +0 -0
  135. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/dateinfo.py +0 -0
  136. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/commands/sched.py +0 -0
  137. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/completion.py +0 -0
  138. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/embedding.py +0 -0
  139. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/active.md +0 -0
  140. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/admin.md +0 -0
  141. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/agenda.md +0 -0
  142. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/alias.md +0 -0
  143. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/ancestors.md +0 -0
  144. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/apply.md +0 -0
  145. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/bulk.md +0 -0
  146. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/cancel.md +0 -0
  147. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/changes.md +0 -0
  148. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/clock.md +0 -0
  149. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/config.md +0 -0
  150. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/date.md +0 -0
  151. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/dateinfo.md +0 -0
  152. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/day.md +0 -0
  153. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/defer.md +0 -0
  154. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/delete.md +0 -0
  155. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/descendants.md +0 -0
  156. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/done.md +0 -0
  157. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/find.md +0 -0
  158. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/focus.md +0 -0
  159. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/index.md +0 -0
  160. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/init.md +0 -0
  161. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/link.md +0 -0
  162. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/metric.md +0 -0
  163. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/migrate.md +0 -0
  164. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/planned.md +0 -0
  165. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/print-completion.md +0 -0
  166. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/priority.md +0 -0
  167. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/prop.md +0 -0
  168. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/query.md +0 -0
  169. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/reindex.md +0 -0
  170. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/relog.md +0 -0
  171. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/reopen.md +0 -0
  172. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/reporting.md +0 -0
  173. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/sched.md +0 -0
  174. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/set.md +0 -0
  175. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/show.md +0 -0
  176. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/spent.md +0 -0
  177. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/start.md +0 -0
  178. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/status.md +0 -0
  179. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/stop.md +0 -0
  180. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/summary.md +0 -0
  181. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/tag.md +0 -0
  182. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/themes.md +0 -0
  183. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/tick.md +0 -0
  184. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/time.md +0 -0
  185. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/tree.md +0 -0
  186. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/unlink.md +0 -0
  187. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/unlog.md +0 -0
  188. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/unset.md +0 -0
  189. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/wait.md +0 -0
  190. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/help/en/window.md +0 -0
  191. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0001_initial_schema.sql +0 -0
  192. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0002_metric_and_log_tag.sql +0 -0
  193. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0003_backfill_checkin_metrics.sql +0 -0
  194. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0004_meta_props_to_typed_logs.sql +0 -0
  195. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0005_clock_table.sql +0 -0
  196. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0006_rename_log_type_to_tag.sql +0 -0
  197. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0007_utc_timestamps.sql +0 -0
  198. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0008_soft_delete_tombstones.sql +0 -0
  199. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0009_tombstone_node_path_view.sql +0 -0
  200. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/migrations/0010_overview_top5_into_goal.sql +0 -0
  201. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/timeutil.py +0 -0
  202. {pyworklog-0.8.0 → pyworklog-0.9.0}/src/worklog/xdg.py +0 -0
  203. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/__init__.py +0 -0
  204. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_alias.py +0 -0
  205. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_aliases.py +0 -0
  206. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_init.py +0 -0
  207. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_timeutil.py +0 -0
  208. {pyworklog-0.8.0 → pyworklog-0.9.0}/tests/test_xdg.py +0 -0
@@ -0,0 +1,11 @@
1
+ .venv
2
+ .git
3
+ __pycache__
4
+ *.pyc
5
+ .pytest_cache
6
+ .coverage
7
+ htmlcov
8
+ *.egg-info
9
+ build
10
+ dist
11
+ *.bak
@@ -44,7 +44,7 @@ jobs:
44
44
  export WORKLOG_DB=$(mktemp -d)/worklog.db
45
45
  uv run wl init
46
46
  uv run wl --version
47
- uv run wl add "ci smoke task" -k task -p A --sched today
47
+ uv run wl add "ci smoke task" -p A --sched today
48
48
  uv run wl log 1 "hello from CI"
49
49
  uv run wl recap "ci smoke recap" # creates today's day skeleton so the overview renders
50
50
  uv run wl show 1
@@ -41,7 +41,7 @@ wl --db /tmp/scratch.db add "..."
41
41
 
42
42
  **Single-package CLI.** `src/worklog/` holds everything: `cli.py` (~5000 lines) is the implementation, `migrations/NNNN_*.sql` is the schema (one initial file at v0.3.0, more added per schema change), `__init__.py` re-exports `__version__`. No further submodule split inside `cli.py` — kept this way intentionally; reconsider only when a feature truly cannot be co-located.
43
43
 
44
- **One polymorphic `node` table.** The `kind` column distinguishes `lifetime / decade / year / quarter / month / week / day / area / project / task / meetlog / habit / signal`; `parent_id` self-reference builds the tree. Two parallel sub-trees hang under `lifetime`: a **responsibility line** (`area → project → task`, PARA-style) and a **time line** (`year → quarter → month → week → day`, time skeleton + each level's `goal` / the day's `summary` reserved-tag logs). Day/week/month views are **log-driven** (`logged_at` + ancestor chain + tags), not via fixed parent-of-task — moving projects between areas does not break per-day views. Tables: `node / tag / log / prop / link / sched / date_meta` + recursive CTE view `v_node_path`.
44
+ **One polymorphic `node` table.** Classification is the orthogonal `type.*` prop namespace (`type.para` area/project/task · `type.date` lifetime…day · `type.habit` · `type.meetlog`); `parent_id` self-reference builds the tree. Two parallel sub-trees hang under `lifetime`: a **responsibility line** (`area → project → task`, PARA-style) and a **time line** (`year → quarter → month → week → day`, time skeleton + each level's `goal` / the day's `summary` reserved-tag logs). Day/week/month views are **log-driven** (`logged_at` + ancestor chain + tags), not via fixed parent-of-task — moving projects between areas does not break per-day views. Tables: `node / tag / log / prop / link / sched / date_meta` + recursive CTE view `v_node_path`.
45
45
 
46
46
  **Path resolution.** Priority: `wl --db PATH` flag → `$WORKLOG_DB` env → `$XDG_DATA_HOME/worklog/worklog.db` (default `~/.local/share/worklog/worklog.db`). Config is `$XDG_CONFIG_HOME/worklog/aliases.ini`. `_resolve_db_path(args)` is the only resolver; `cmd_config` displays which branch fired.
47
47
 
@@ -6,6 +6,38 @@ The autogenerated `Commits since vX.Y.Z` block on each GitHub Release captures e
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0] - 2026-06-18
10
+
11
+ ### Breaking
12
+ - **`kind` retired — classification is now the orthogonal `type.*` prop namespace** (`type.para` area/project/task · `type.date` level · `type.habit` · `type.meetlog`); a node can hold several at once. Migration **0011** backfills + verifies a lossless round-trip + drops the `node.kind` column, auto-run on first use.
13
+ - **`-k/--kind` gone from `wl add` / `wl node edit`** — set the role with `--para area|project` (bare = task), other types with `--prop type.<x>` (e.g. `type.habit`, `type.date=day`); reclassify later via `wl set type.<x>`.
14
+ - **`--kind` filter gone** from `ls`/`tree`/`day`/`logs`/`agenda`/`find` — filter by `--para area|project|task` or `--prop type.<x>`; `wl find` gains `--para`, and `wl checkin --all-types` (renamed from `--all-kinds`) still covers habit + task + meetlog.
15
+ - **JSON drops the collapsed `kind` for an orthogonal `type` object** — every `-o json` (`show`/`ls`/`tree`/`day`/`projects`/`summary`) carries `type` (the `type.*` facets, prefix stripped, e.g. `{"para":"project","habit":true}`; bare task → `{}`).
16
+ - **`wl kinds` → `wl types`** — lists the raw `type.*`/`date.*` props grouped by key (no collapse); a node shows under every facet it carries.
17
+ - **`wl import` / `wl apply` drop the `kind` field/token** — classify via the real props: JSON `{"props": {"type.para": "project"}}` (or `type.date`/`type.habit`/`type.meetlog`; bare = task), or an `@prop type.para=project` sub-line (no `[project]` token on the node line).
18
+
19
+ ### Added
20
+ - **`wl add --para {area,project,task}`** — name a responsibility role at creation (writes the `type.para` prop, same flag as `wl ls --para`); `wl add --prop K=V` sets props at creation. Every new node now also populates the `type.*` namespace (the successor to `kind`).
21
+ - **`wl find --para {area,project,task}`** — filter full-text search results by responsibility role.
22
+ - **`wl log show #L282`** — print one log's full, untruncated body (lists truncate to one line).
23
+ - **`wl retag #L282 <tag>`** — change a log's tag (`note`/`-`/empty clears to a plain note).
24
+ - **`wl ls --root <id>`** — flat list of a whole subtree (all descendants), vs `--parent` (one level).
25
+ - **`wl metric ls --tag X`** with no node — find a metric tag across every node.
26
+ - **`wl help body`** — node.body (long description) vs log.body (per-entry text).
27
+ - **`wl recap --diff`** — list the notes logged after the recap (excludes checkin/metric noise).
28
+ - **`wl reindex --incremental`** — embed only nodes new/changed since the last index + drop deleted ones (cheap day-to-day top-up vs a full rebuild).
29
+ - **Background auto-reindex** — after any write, the index is incrementally updated so `wl query` stays fresh without a manual reindex. On by default; disable with `[index] auto_reindex = false` (config.ini) or `WORKLOG_AUTO_REINDEX=0`.
30
+
31
+ ### Changed
32
+ - **`wl relation <id> <ids…>` defaults to `related`** (omit the type word); split-from/into still explicit.
33
+
34
+ ### Fixed
35
+ - **`wl show` multi-line `body`** — continuation lines now indent under the value, not column 0.
36
+ - **`wl query` Chinese recall** — a node matching every query term now leads; vector list capped before RRF so its tail can't bury exact hits.
37
+ - **`wl query` highlight** — highlights each segmented term, so a non-contiguous match (微信…导出) still lights up.
38
+ - **`wl query` stale-index hint** — warns on stderr `⚠ N node(s) not indexed — run wl reindex`.
39
+ - **Silence jieba's `SyntaxWarning`** (invalid escape sequences, flagged by Python 3.12+) at import — jieba's bug, was noise on stderr.
40
+
9
41
  ## [0.8.0] - 2026-06-14
10
42
 
11
43
  ### Breaking
@@ -21,6 +53,7 @@ The autogenerated `Commits since vX.Y.Z` block on each GitHub Release captures e
21
53
 
22
54
  ### Added
23
55
  - **`wl query` / `wl reindex` — hybrid (semantic + keyword) search.** `wl reindex` embeds each node's chunks (head + per-log) via an OpenAI-compatible server (default local Ollama) into a sidecar LanceDB; `wl query` RRF-fuses best-chunk cosine with a jieba-segmented, `[synonyms]`-expanded keyword match — paraphrases *and* exact names both surface — showing the matched passage, term-highlighted (`--limit`/`--threshold`/`-o json`). Configurable backend + `query_prompt` template (`wl config`). Pluggable, graceful degradation: the `semantic` extra (LanceDB + jieba) is the fast path, but absent it `wl query`/`reindex` auto-fall-back to a pure-Python SQLite vector store + `\w+` segmentation — so semantic search works on every Python 3.9–3.14 / platform with no required extras (slower where there's no LanceDB wheel: Intel macOS, musl, …).
56
+ - **`wl config init`** — writes a commented `config.ini` template (embedding backend / `query_prompt` / `api_key`); never overwrites an existing file.
24
57
  - **`wl help` is a short overview; `wl help --all` lists every topic.**
25
58
  - **Structured goal targets — `wl goal "..." <ids>`** records the task nodes a goal targets (order = priority) as queryable `goal` metrics; shown as `[done/total]` in `wl day`/`wl goal`.
26
59
  - **`wl show` timeline marks a log's tag** — `✎ goal`/`✎ summary`/`✎ <custom>` vs plain `✎ log`.
@@ -205,7 +238,9 @@ Initial open-source release of `worklog-cli` (`wl`).
205
238
  - **Vault integration** — `wl link` ↔ Obsidian docs (`[[doc]]`).
206
239
  - Bilingual docs (EN + 中文); MIT License.
207
240
 
208
- [Unreleased]: https://github.com/xyb/worklog/compare/v0.7.0...HEAD
241
+ [Unreleased]: https://github.com/xyb/worklog/compare/v0.9.0...HEAD
242
+ [0.9.0]: https://github.com/xyb/worklog/compare/v0.8.0...v0.9.0
243
+ [0.8.0]: https://github.com/xyb/worklog/compare/v0.7.0...v0.8.0
209
244
  [0.7.0]: https://github.com/xyb/worklog/compare/v0.6.0...v0.7.0
210
245
  [0.6.0]: https://github.com/xyb/worklog/compare/v0.5.0...v0.6.0
211
246
  [0.5.0]: https://github.com/xyb/worklog/compare/v0.4.0...v0.5.0
@@ -20,7 +20,7 @@ worklog's whole job is to **structure** the work log so information can be preci
20
20
 
21
21
  The tool must be simple enough to **use without reading the manual**, for both humans and AI. AI faces a plain-text interface; complexity makes it err.
22
22
 
23
- - Keep the **kinds of records hung under a node** restrained (fewer is better): each extra kind is one more concept to remember and one more block to render — heavier, harder to grasp.
23
+ - Keep the **types of records hung under a node** restrained (fewer is better): each extra type is one more concept to remember and one more block to render — heavier, harder to grasp.
24
24
  - Self-check any new design with three questions: (1) how many new concepts does it add? (2) does using it force AI/humans to make a choice ("A or B?")? (3) can you guess it right without docs? If any answer is poor, simplify further.
25
25
  - When G1 and G2 conflict, find the "structured AND fewest-concepts" answer — don't pile on new tables / fields / commands just to be structured.
26
26
 
@@ -38,7 +38,7 @@ worklog deliberately stays **dependency-light and low-abstraction**: runtime is
38
38
  - Verb first: `wl <verb> [args] [--flags]`
39
39
  - Subcommand names: lowercase single words — `add` / `done` / `tree` / `projects` / `changes` / `summary` / `focus` …
40
40
  - Commands operating on a single node take `id` (int) as the first positional arg: `wl show 42` / `wl done 42` / `wl log 42 "..."`
41
- - Long flags `--xxx`; short flags only for the highest-frequency ones (`-k` kind / `-p` priority / `-t` tag)
41
+ - Long flags `--xxx`; short flags only for the highest-frequency ones (`-p` priority / `-t` tag)
42
42
  - Missing node: always `sys.exit(f"✗ ... #{id} not found")` with non-zero exit code
43
43
 
44
44
  ### 1.1 Command taxonomy (primitive CRUD / composite helper / view)
@@ -134,7 +134,7 @@ metric + done), and the compound `add --log/--done/--at/--link/--sched/--metric`
134
134
  **C. View** — read-only cross-entity rendering, no single-row CRUD: `ls` / `tree` /
135
135
  `day` / `agenda` / `projects` / `changes` / `summary` / `focus` / `ancestors` /
136
136
  `descendants` / `find` / `logs` / `show`. These share the unified filter (§ shared
137
- `--tag/--kind/--status/--priority/--prop`), time-window (§8) and `--by` (§9) conventions. `--prop`
137
+ `--tag/--para/--status/--priority/--prop`), time-window (§8) and `--by` (§9) conventions. `--prop`
138
138
  is repeatable (AND) and honors the namespace convention — exact `K=V` (matches a member of a
139
139
  comma-joined value), key existence `K`, and `GROUP.` / `GROUP.*` prefix (see §2 / `wl help prop`).
140
140
 
@@ -161,14 +161,14 @@ two ways to invoke the same operation never drift in discoverability, only in ke
161
161
 
162
162
  ## 2. Data model (`src/worklog/migrations/`)
163
163
 
164
- A single `node` table carries everything; the `kind` field discriminates type; `parent_id` self-reference builds the tree. The schema is delivered as numbered SQL migrations under `src/worklog/migrations/NNNN_*.sql`; `PRAGMA user_version` tracks the highest applied migration. `ensure_db()` auto-applies pending migrations on every command; `wl migrate` is the explicit form. Before applying, `run_migrations` snapshots an **existing** DB (`user_version` > 0) to a same-dir `<db>.pre-v<N>.bak` so a bad migration is recoverable (a fresh init at v0 has no data to protect, so it's skipped); a backup that can't be written aborts the migration rather than risking unrecoverable loss. See `src/worklog/migrations/0001_initial_schema.sql` for the initial layout and `README.md` for the high-level picture.
164
+ A single `node` table carries everything; classification is the orthogonal `type.*` prop namespace (no dedicated column); `parent_id` self-reference builds the tree. The schema is delivered as numbered SQL migrations under `src/worklog/migrations/NNNN_*.sql`; `PRAGMA user_version` tracks the highest applied migration. `ensure_db()` auto-applies pending migrations on every command; `wl migrate` is the explicit form. Before applying, `run_migrations` snapshots an **existing** DB (`user_version` > 0) to a same-dir `<db>.pre-v<N>.bak` so a bad migration is recoverable (a fresh init at v0 has no data to protect, so it's skipped); a backup that can't be written aborts the migration rather than risking unrecoverable loss. See `src/worklog/migrations/0001_initial_schema.sql` for the initial layout and `README.md` for the high-level picture.
165
165
 
166
166
  > **Migration-authoring rule**: the runner wraps each file in one `BEGIN/COMMIT` (so a mid-script failure rolls the whole file back). Migration files must therefore **not** contain their own `BEGIN`/`COMMIT`.
167
167
 
168
- - **kind values**: `lifetime / decade / year / quarter / month / week / day / area / project / task / meetlog / habit / signal` (extensible, but new kinds should have a clear place in tree / projects / summary classification)
169
- - **status only applies to task / habit / meetlog**; time-hierarchy kinds (year/month/...) and project kind leave status NULL
168
+ - **classification values** (orthogonal `type.*` props, no single column): `type.para` = PARA role `area / project / task`; `type.date` = time level `lifetime / decade / year / quarter / month / week / day`; `type.habit`; `type.meetlog`; custom `type.<x>` (extensible, but a new type should have a clear place in tree / projects / summary classification). A node may hold several at once; a single representative token is derived by `node_type` / `node_type_from_props` (precedence para > date > habit > meetlog > custom > task)
169
+ - **status only applies to task / habit / meetlog**; time-hierarchy / area / project nodes leave status NULL
170
170
  - Tables: `node / tag / log / metric / clock / prop / link / sched / date_meta` + derived view `v_node_path`
171
- - **`node → log → metric` spine** (the log-centric core): a `node` has many `log`s; a `log` (carrying a `tag` — `note`/`goal`/`summary`/`metric`(carrier)/… , NULL = plain note) has 0..N `metric`s. A `metric` is a structured datapoint (`tag` = what it is e.g. `glucose`/`pullups`/`checkin`, or `goal` = a goal log's priority-ordered target node ids; `value_num`/`value_text`/`unit`/`note`/`at`). **`tag` is the uniform classification field across all three** — node (a multi-value label set), log (its role, single-value), metric (its kind, single-value); same word, different scopes, SQL-unambiguous. A metric **must hang off a log** (`metric.log_id` NOT NULL) — so every datapoint has a log carrier; a `CHECK` requires a value (pure markers store `value_num=1`). `metric.node_id` is denormalized for join-free per-node queries (no FK; triggers keep it equal to the carrier log's node). CRUD surface: `wl metric add/ls/edit/rm` (`add` without `--on-log` creates a carrier log; a value-less marker is stored as `value_num=1`); `--metric` on `wl log`/`wl add` and `metrics` in `wl import` attach datapoints inline. Habit "done today" = a `tag=checkin` metric that day (written by `wl tick`/`wl checkin`), not "any log exists".
171
+ - **`node → log → metric` spine** (the log-centric core): a `node` has many `log`s; a `log` (carrying a `tag` — `note`/`goal`/`summary`/`metric`(carrier)/… , NULL = plain note) has 0..N `metric`s. A `metric` is a structured datapoint (`tag` = what it is e.g. `glucose`/`pullups`/`checkin`, or `goal` = a goal log's priority-ordered target node ids; `value_num`/`value_text`/`unit`/`note`/`at`). **`tag` is the uniform classification field across all three** — node (a multi-value label set), log (its role, single-value), metric (its type, single-value); same word, different scopes, SQL-unambiguous. A metric **must hang off a log** (`metric.log_id` NOT NULL) — so every datapoint has a log carrier; a `CHECK` requires a value (pure markers store `value_num=1`). `metric.node_id` is denormalized for join-free per-node queries (no FK; triggers keep it equal to the carrier log's node). CRUD surface: `wl metric add/ls/edit/rm` (`add` without `--on-log` creates a carrier log; a value-less marker is stored as `value_num=1`); `--metric` on `wl log`/`wl add` and `metrics` in `wl import` attach datapoints inline. Habit "done today" = a `tag=checkin` metric that day (written by `wl tick`/`wl checkin`), not "any log exists".
172
172
  - **Design rule — keep `log` thin; new per-log data goes on a `metric`, not a new log column.** The `log` table stays a small fixed shape (`node_id`/`logged_at`/`body`/`tag`). When some feature wants to attach structured data to a log event (a measurement, an id, a category, a flag), the answer is a `metric` hanging off that log — never a widened `log` row. A metric is already typed (`tag` + `value_num`/`value_text`/`unit`/`note`/`at`), queryable (`wl metric ls --tag …`), and append-only, so it carries arbitrary per-event fields without bloating every log. Example: `wl agent --record` records a bind as **two metrics on one carrier log** — `agent_session` (`value_text` = full sid) and `agent` (`value_text` = the runtime name — claude / cursor / codex, from `$WL_AGENT` / `--agent`) — not as `log.session_id` / `log.agent` columns. Each value is its own queryable datapoint (`wl metric ls <id> --tag agent_session` / `--tag agent`). This keeps the log body human-readable and the schema stable (G3). The live pointer is the `agent_session.<agent>` prop (the key suffix carries the agent too); session lookups match `key LIKE 'agent_session.%'` since the sid alone is unique. (The metric tag string `agent_session` mirrors the prop namespace; the Python constant is `_SESSION_METRIC_TAG`.)
173
173
  - **Decision rule — where does a fact go: `prop` vs `metric` vs `log`?** Sort by *"will I filter / group / aggregate over it across nodes?"* and by *cardinality*:
174
174
  - **`prop`** — a **single-value query dimension** (`PRIMARY KEY (node_id, key)` → one value per key, overwrite-in-place). Use it for the few attributes you slice the whole tree by: `owner`, `project`, `linear-id`, or **the one identifying ref a task maps to** (the release it shipped in → `release=v0.7.0`, so "which tasks shipped in v0.7.0" is one `prop` query; the single PR a one-shot task *is*). Props are scarce and high-value — don't flood the key-space with process noise.
@@ -177,15 +177,15 @@ A single `node` table carries everything; the `kind` field discriminates type; `
177
177
  - Worked example (commit / PR / release ↔ task): the **many** intermediate commits of a dev task → `log` (add a `commit` **metric** if you want them structured); a task that maps to **one** identifying PR/commit, or its **release** → `prop` (so it's filterable/countable). The dividing question is value-to-query, not "can it physically fit".
178
178
  - **Namespaced prop keys (`group.member`)** — a prop key may use a dot to group related single-value props under a shared prefix (`agent_session.claude` / `agent_session.cursor`; external ids per system `ext.linear` / `ext.github`). Each full key is still one single-value prop (`PRIMARY KEY (node_id, key)`); the namespace groups *sibling* keys — it does **not** make a key multi-valued. The convention's payoff is **prefix lookup**: one `key LIKE 'group.%'` finds every member across nodes, so prop-based features (filter / summary / stats) operate on a whole namespace, not just an exact key. **All prop query surfaces must honor it**: exact key (`k=v`), key existence (`k`), and namespace prefix (`group.` / `group.*`). Reserve it for a logical dimension with several named slots; keep flat keys (`owner`, `release`) flat. First user: `wl agent` (the live binding is the `agent_session.<agent>` prop; sid lookups match `key LIKE 'agent_session.%'`).
179
179
  - **Task↔task relations are `relation.*` props (`wl relation`)** — derivation / association *across* the tree (this task was split out of / into / relates to that one), distinct from **ancestors** (the parent/child hierarchy). Stored as namespaced props `relation.split_from` / `relation.split_into` / `relation.related`, each a **comma-separated id list** (a relation is naturally many-per-node, so the value is multi-valued — the one place a prop value holds a list rather than a single scalar). `split-from` ↔ `split-into` are inverses; `related` is symmetric. `wl relation` writes **both sides** (so either node's `wl show` displays it) and the resolved view *also* derives the reverse from other nodes' props, so a hand-set one-sided value still reads bidirectionally. `wl show` renders a dedicated `relations:` block (and `-o json` a resolved `relations` object); the raw `relation.*` props are hidden from the generic props block to avoid double display. Why a prop and not a `link`-style table: it rides the existing namespace + `wl ls --prop relation.` query machinery for free (G3) — no new table or join.
180
- - **A goal's structured targets are `goal` metrics on the goal log** — a `goal` log (goal/summary are `log` rows with a reserved `tag`) can carry, supplied **explicitly at write time** (`wl goal "ship X" 12 34` / `wl goal set <node> "..." 7 9 3`), the task/project node ids it aims to deliver — stored as one `goal` metric per id on the log (`metric.tag='goal'`, `value_num=node id`, **insertion order = priority**, any count). wl **never parses them from the prose** (text extraction is too fragile); omit them and the goal is plain free text. The metric tag is the bare word `goal` (same as the carrier log's tag) — no per-level suffix, because the carrier log's `node.kind` (day/week/month/year) already says the level. So the plan↔node link is **queryable** off the metric table (`tag='goal'`, `value_num=<id>` = which goals target a node; narrow by `node.kind` for the level) rather than buried in prose, and history-preserving (each goal edit appends a log with its own target snapshot; `_log_goals` reads the latest). Reuses the existing `metric` entity — no new table.
180
+ - **A goal's structured targets are `goal` metrics on the goal log** — a `goal` log (goal/summary are `log` rows with a reserved `tag`) can carry, supplied **explicitly at write time** (`wl goal "ship X" 12 34` / `wl goal set <node> "..." 7 9 3`), the task/project node ids it aims to deliver — stored as one `goal` metric per id on the log (`metric.tag='goal'`, `value_num=node id`, **insertion order = priority**, any count). wl **never parses them from the prose** (text extraction is too fragile); omit them and the goal is plain free text. The metric tag is the bare word `goal` (same as the carrier log's tag) — no per-level suffix, because the carrier log's node type (day/week/month/year) already says the level. So the plan↔node link is **queryable** off the metric table (`tag='goal'`, `value_num=<id>` = which goals target a node; narrow by node type for the level) rather than buried in prose, and history-preserving (each goal edit appends a log with its own target snapshot; `_log_goals` reads the latest). Reuses the existing `metric` entity — no new table.
181
181
  - **Deliberate design — `wl add` / `wl log` stamp the current local time in their success line** (`✓ #1 task '…' @2026-06-09 20:09`). Intentional, primarily an **AI affordance**: an assistant's sense of "today" drifts to the date its session started and goes stale over a long session, so it mis-dates backfills and reasoning. Echoing the real wall-clock on every content-creating command re-anchors the caller's clock to *now*. (`local_now()`, minute precision.)
182
- - **Reserved-tag logs are history-preserving**: `goal` (forward, any time level) and `summary` (backward recap) are `log.tag` logs (latest = current; each edit appends), with their own `wl goal set/ls/rm` CRUD group (`--summary` flag targets the summary); also written by bare `wl goal`/`wl recap` (today-auto) and the key-routed `wl set <node> <key>` shortcut. A goal is the **same `goal` tag at every level** — the node's kind (day/week/month/year) is the level; the former week `overview` and month `top5` collapsed into it (migration 0010). (`prop` is back to truly-static single-value attributes — a separate store.) In the `wl day` header the day's goal + recap plus the ancestor week's & month's goal each render with a distinct marker (`_DAY_MARKERS`: 🎯 goal · 📝 recap · 📅 this week · ⭐ this month) through `_header_blockquote`, which prefixes only the FIRST line with `> ` and aligns every continuation (soft-wrap / embedded newline) with plain spaces under the `> ` content column (cleaner than repeating `> `), wrapping by display width like `_node_line`. Each shown goal then lists its structured target nodes below it — numbered, status-marked (`_emit_goal_targets`, shared by `wl day` / `wl goal` / `wl goal ls` so they render identically) — and a ` [done/total]` achievement tag (`_goal_progress`/`_goal_counts`: counts the structured targets' DONE/CANCELED, falling back to prose `#id`/`WL#id` refs when a goal has no structured targets; ✅/🟡/⬜).
182
+ - **Reserved-tag logs are history-preserving**: `goal` (forward, any time level) and `summary` (backward recap) are `log.tag` logs (latest = current; each edit appends), with their own `wl goal set/ls/rm` CRUD group (`--summary` flag targets the summary); also written by bare `wl goal`/`wl recap` (today-auto) and the key-routed `wl set <node> <key>` shortcut. A goal is the **same `goal` tag at every level** — the node's type (day/week/month/year) is the level; the former week `overview` and month `top5` collapsed into it (migration 0010). (`prop` is back to truly-static single-value attributes — a separate store.) In the `wl day` header the day's goal + recap plus the ancestor week's & month's goal each render with a distinct marker (`_DAY_MARKERS`: 🎯 goal · 📝 recap · 📅 this week · ⭐ this month) through `_header_blockquote`, which prefixes only the FIRST line with `> ` and aligns every continuation (soft-wrap / embedded newline) with plain spaces under the `> ` content column (cleaner than repeating `> `), wrapping by display width like `_node_line`. Each shown goal then lists its structured target nodes below it — numbered, status-marked (`_emit_goal_targets`, shared by `wl day` / `wl goal` / `wl goal ls` so they render identically) — and a ` [done/total]` achievement tag (`_goal_progress`/`_goal_counts`: counts the structured targets' DONE/CANCELED, falling back to prose `#id`/`WL#id` refs when a goal has no structured targets; ✅/🟡/⬜).
183
183
  - **`clock` is structured time tracking**: `clock(node_id, start_at, end_at, elapsed_sec)`, written by `wl start`/`stop`/`spent`/`wait` — replaces the old `CLOCK_IN`/`CLOCK_OUT` log-body convention. Durations are summed from `elapsed_sec`, not parsed from text.
184
184
  - **Two parallel trees, both hung under lifetime**:
185
185
  - **Responsibility line**: `lifetime → area → project → task` (PARA model: area is a cross-time responsibility domain, projects belong to areas, tasks belong to projects)
186
186
  - **Time line**: `lifetime → year → quarter → month → week → day` (carries the time skeleton + reserved-tag logs: each level's `goal`, the day's `summary` recap)
187
187
  - The recap (`summary` log) carries its own `logged_at` = write time. `wl day` shows "(written ...)" and, if there are later plain-note logs that day (`tag IS NULL`), warns "⚠ N changes after recap, consider rewriting". (Replaces the old `summary_at` prop.)
188
- - **Projects no longer hang under months** (legacy design did; migrated to areas). Day/month/week views derive from **log's `logged_at`** (time dimension) and **kind/tag/ancestor chain** (domain dimension) — so moving projects under areas does not affect any per-day or per-project view.
188
+ - **Projects no longer hang under months** (legacy design did; migrated to areas). Day/month/week views derive from **log's `logged_at`** (time dimension) and **type/tag/ancestor chain** (domain dimension) — so moving projects under areas does not affect any per-day or per-project view.
189
189
 
190
190
  ## 3. State machine (#+TODO style)
191
191
 
@@ -275,7 +275,7 @@ Both `tree` and `summary` support `--by`; future aggregation commands follow the
275
275
 
276
276
  ### 9.1 `wl tree` default behavior (anti-blast)
277
277
 
278
- Fully expanding both trees would print thousands of lines. **Bare `wl tree` (no --root/--kind/--depth/--by) is a dedicated overview view `_print_default_tree`**:
278
+ Fully expanding both trees would print thousands of lines. **Bare `wl tree` (no --root/--para/--depth/--by) is a dedicated overview view `_print_default_tree`**:
279
279
 
280
280
  - **Time line expanded to today**: year → quarter → month → week → today (only the path to today, no other months/weeks/days), and today's day node lists **only tasks with a log today** (task/habit/meetlog, no log body). You see the current month yet stay focused on today.
281
281
  - **Areas are one level only**: 7 areas show only area names, no project expansion.
@@ -284,7 +284,7 @@ Fully expanding both trees would print thousands of lines. **Bare `wl tree` (no
284
284
 
285
285
  `_print_tree` rules:
286
286
  - **Depth cap**: with `--root`, default 3; without root (`--depth` explicit), as given; `_print_tree(max_depth)` truncates beyond
287
- - **Time nodes sort by date** (`_tree_children`): kind ∈ time-types sort by `title` (date) ascending; others by priority → id (otherwise W22 sorts before W18)
287
+ - **Time nodes sort by date** (`_tree_children`): type ∈ time-levels sort by `title` (date) ascending; others by priority → id (otherwise W22 sorts before W18)
288
288
  - **Day node expansion = day's activity** (`_print_day_activity`): after areas changeover, day has no real children — instead expand "tasks with logs that day + only that day's logs" (log-driven, same source as `wl day`). **Tasks should not hang under day** (hang under project); day content is derived from log dates.
289
289
 
290
290
  ## 10. Project ↔ task association (`_project_members`)
@@ -357,10 +357,10 @@ Reuse these when a new command needs the functionality; do not write another cop
357
357
  ```json
358
358
  {
359
359
  "add": [
360
- { "ref": "p1", "title": "...", "kind": "project", "priority": "A",
361
- "tags": [...], "props": {...}, "links": [...], "logs": [...],
360
+ { "ref": "p1", "title": "...", "priority": "A",
361
+ "tags": [...], "props": {"type.para": "project"}, "links": [...], "logs": [...],
362
362
  "children": [ {... nested child nodes ...} ] },
363
- { "title": "...", "kind": "task", "parent_ref": "p1" }
363
+ { "title": "...", "parent_ref": "p1" }
364
364
  ],
365
365
  "update": [
366
366
  { "id": 45, "status": "DONE", "parent": 6, "add_tags": [...], "remove_tags": [...], "add_logs": [...], "add_links": [...] }
@@ -370,6 +370,7 @@ Reuse these when a new command needs the functionality; do not write another cop
370
370
 
371
371
  Conventions:
372
372
 
373
+ - **Classification = `props` with `type.*` keys**: `{"type.para":"project"}` (or `area`/`task`), `{"type.date":"day"}` (week/month/…), `{"type.habit":"true"}`, `{"type.meetlog":"true"}`; a bare node (no `type.*`) is a plain task. A `type.date` prop auto-completes its `date.period`/span from the title.
373
374
  - **Two parent-child notations coexist**: `children` nesting (parent id auto-propagates to children — natural for time-hierarchy loads) + `ref`/`parent_ref` (in-batch temporary references — flat cross-section loads). `ref` must be defined before use.
374
375
  - **`status=DONE` auto-writes `closed_at`** (in both add and update)
375
376
  - **Update-able fields**: `status` / `priority` / `title` / `scheduled_at` / `deadline_at` / `body` / `parent` (move, validates existence) + `add_tags` / `remove_tags` / `add_logs` / `add_links`. ⚠️ Keys not in the whitelist are silently ignored — `parent` was once missing from the whitelist and caused moves to silently fail (later fixed); when adding fields, sync the whitelist.
@@ -388,8 +389,8 @@ When adding import fields (new node attribute / update operation), keep `_import
388
389
 
389
390
  ### 18.1 Add `+` / delete `-` / anchor ` `: the node line
390
391
 
391
- `<prefix><indent>[marker] [#pri] #id [kind] title :tags:` (marker required; `+` has no `#id`; `-` / anchor must have `#id`).
392
- Rich-field sub-lines: `@log <text>` / `@link <doc>` / `@prop k=v` (attached to the preceding `+` / anchor node).
392
+ `<prefix><indent>[marker] [#pri] #id title :tags:` (marker required; `+` has no `#id`; `-` / anchor must have `#id`).
393
+ Rich-field sub-lines: `@log <text>` / `@link <doc>` / `@prop k=v` (attached to the preceding `+` / anchor node). Classification is set via `@prop type.*` (e.g. `@prop type.para=project`), not a token on the node line.
393
394
  marker → status: `[ ]`=TODO, `[x]`=DONE, `[/]`=DOING, `[>]`=LATER, `[?]`=WAIT, `[-]`=CANCELED.
394
395
 
395
396
  ```
@@ -459,7 +460,7 @@ Terminal highlighting goes through `rich`; can be turned off, can switch themes,
459
460
  ### 19.1 Switches and detection
460
461
 
461
462
  - Global flags (before any subcommand): `--color {auto,always,never}` + `--theme {auto,dark,light,mono}` (theme choices = `["auto"] + list(THEMES)`; new palettes are auto-added to choices) + `--width {full,help,N}` / `$WORKLOG_WIDTH` (output width cap: `full` = fill terminal [default], `help` = the `--help` cap `HELP_MAX_WIDTH`, `N` = N columns). Resolved by `_resolve_width_cap` → `_set_width_cap`; `_term_width()` returns `min(terminal, cap)`, so all width-sensitive rendering (`_truncate_log_body`, title fits) respects it from one lever.
462
- - `--title {wrap,clip}` / `$WORKLOG_TITLE` — how a node title too wide for the line renders in `_node_line`: `wrap` (default) folds onto multiple lines, continuation lines **hang-indented** to the title column (`prefix_cols` = display width of the plain `indent+marker+pri+id+kind` prefix), so a long title never breaks the tree/list indentation; `clip` keeps one line, truncating with `…` (same budget as `_truncate_log_body`). Resolved by `_resolve_title_mode` → `_set_title_mode`; the wrap itself is `_wrap_display(text, cols)` — display-width-aware (East-Asian), breaks at spaces and hard-breaks a token (e.g. a spaceless CJK run) wider than the line. Anything but `clip` resolves to `wrap`, so the safe default always wins.
463
+ - `--title {wrap,clip}` / `$WORKLOG_TITLE` — how a node title too wide for the line renders in `_node_line`: `wrap` (default) folds onto multiple lines, continuation lines **hang-indented** to the title column (`prefix_cols` = display width of the plain `indent+marker+pri+id+type` prefix), so a long title never breaks the tree/list indentation; `clip` keeps one line, truncating with `…` (same budget as `_truncate_log_body`). Resolved by `_resolve_title_mode` → `_set_title_mode`; the wrap itself is `_wrap_display(text, cols)` — display-width-aware (East-Asian), breaks at spaces and hard-breaks a token (e.g. a spaceless CJK run) wider than the line. Anything but `clip` resolves to `wrap`, so the safe default always wins.
463
464
  - `-o/--output {text,json}` — the **structured-output** surface, a shared `output_parent` parser on the commands that serialize: `show` / `ls` / `logs` / `projects` / `day` / `tree` / `summary` (a command without it rejects `-o` rather than silently printing text). `text` (default) is the rich rendering; `json` prints via `json.dumps(ensure_ascii=False, indent=2)` (plain `print`, never `out()` — machine output isn't styled/wrapped). `show` → `_node_to_dict` (full node + relations: tags / ancestors / props / links / schedule / children / logs / metrics / clock, no elision; single id → object, several → array). `ls` → array of `_node_summary_dict` (compact: identity + filter/sort fields + tags; filters apply, the display 20-cap is skipped, explicit `--limit`/`--top` honored). `logs` → array of log rows. `projects` → array of project summaries + `counts` (done/doing/pending/total) + `latest_activity`. `day` → `{date, weekday, nature, node_id, goal, goal_targets ([{id,title,status}], the goal's structured target nodes in priority order), goal_progress, summary, summary_at, week_goal, month_goal (the day node's reserved-tag logs, flattened in — only present keys), tasks[{…, logs, planned, clock_min}], clock_min_total}`. `tree` → the nested structural subtree (`--root` or top-level forest, each node + `children[]`, depth-bounded; ignores the text `--by`/filter sugar). `summary` → `{since, until, totals, by_direction, done[], pending[]}`. Empty result → `[]`. Field names mirror the DB columns (stable as an API contract); `*_at` are UTC instants verbatim, `*_date` local calendar days. Models the k8s `-o json` / `gh --json` pattern: let an AI/script pull an exact field instead of parsing the text view (G3).
464
465
  - Env fallback: `$WORKLOG_COLOR` (same values as `--color`), `$WORKLOG_THEME`, `$NO_COLOR` (standard, any value disables color)
465
466
  - `--color auto` (default) decides: `rich available AND stdout.isatty() AND no $NO_COLOR` → enabled. So pipes / redirects / test StringIO auto-downgrade to plain text, **tests need no special handling.**
@@ -470,7 +471,7 @@ Terminal highlighting goes through `rich`; can be turned off, can switch themes,
470
471
  **There is no palette named `default`** — `default` was once a fourth standalone palette and had confused semantics (neither dark nor light). Now there are three real palettes + one `auto` selector.
471
472
 
472
473
  `THEMES` dict: each palette maps semantic names to rich styles. Semantic names (not color names), full set in `_THEME_KEYS`:
473
- `done/doing/later/wait/todo/canceled` (status), `pri_a/pri_b/pri_c` (priority), `id/kind/tag/hit/header/meta/planned/clock`.
474
+ `done/doing/later/wait/todo/canceled` (status), `pri_a/pri_b/pri_c` (priority), `id/type/tag/hit/header/meta/planned/clock`.
474
475
 
475
476
  - `dark`: dark background, `bright_*` for contrast
476
477
  - `light`: light background, deep saturated colors (`green4` / `red3` / `dark_orange3` / `dark_cyan` / `purple` / `grey42` …), avoiding bright colors that wash out on white
@@ -491,7 +492,7 @@ out(_c("✓", "done") + " " + _c(f"#{id}", "id") + " " + _c(title))
491
492
 
492
493
  - `_CONSOLE is None` (plain text): `_c` returns `str(text)` as-is, zero overhead
493
494
  - Styled: `_c` **first escapes content via `rich.markup.escape`** (so titles containing `[x]` / `[[doc]]` aren't eaten as markup), then wraps with `[style]…[/style]`; `style=None` only escapes, no color wrap
494
- - **Iron rule: any fragment going into `out()` that may contain `[` `]` (marker `[x]` / priority `[#A]` / kind `[day]` / wikilink `[[doc]]` / timestamp `[ts]`) must pass through `_c`.** Throwing a raw bracket-containing string into `out()` triggers `rich.MarkupError` or silent eating.
495
+ - **Iron rule: any fragment going into `out()` that may contain `[` `]` (marker `[x]` / priority `[#A]` / type `[day]` / wikilink `[[doc]]` / timestamp `[ts]`) must pass through `_c`.** Throwing a raw bracket-containing string into `out()` triggers `rich.MarkupError` or silent eating.
495
496
 
496
497
  ### 19.4 `out()` replaces `print()`
497
498
 
@@ -515,10 +516,10 @@ When adding output / changing colors, keep `_init_console` / `_detect_bg_is_dark
515
516
  - Canonical formats are validated directly (illegal date/month/week raises `ValueError`, caught at dry-run, no bad data lands)
516
517
  - Relative words normalize: `today` → date, `tomorrow` → date, `next-week` → `YYYY-Www`, `next-month` → `YYYY-MM`, `next-quarter` → `YYYY-Qn`, `someday` → `someday`
517
518
  - Unrecognized → `ValueError` with valid-format hint (no silent swallow)
518
- - **Granularity `_sched_kind(s)`** / **sort `_sched_sort_key(s)`**: sort key = `(anchor start date, granularity rank)`; fuzzy values anchor to start of their time slot (`2026-06` → `2026-06-01`, `2026-Q3` → `2026-07-01`); `someday` anchors far future (sorts last); same anchor → finer granularity first
519
+ - **Granularity `_sched_level(s)`** / **sort `_sched_sort_key(s)`**: sort key = `(anchor start date, granularity rank)`; fuzzy values anchor to start of their time slot (`2026-06` → `2026-06-01`, `2026-Q3` → `2026-07-01`); `someday` anchors far future (sorts last); same anchor → finer granularity first
519
520
  - **Display `_sched_display(s)`**: precise date this year shows `MM-DD`; fuzzy shows as-is (`2026-06` / `2026-Q3` / `someday`); `_node_line(sched=True)` renders `@<display>` (planned style highlight; prefix `@` not emoji — 📅 is loud, `@` is light and separates well in plain-text mode); ls / day / summary turn `sched=True` on by default
520
521
 
521
- When changing scheduled-time logic, keep `_norm_sched` / `_sched_kind` / `_sched_anchor` / `_sched_sort_key` / `_sched_display` / `cmd_add` / `cmd_defer` / `_import_node` / `_validate_fieldop` / `_exec_update` / `_node_line` in sync.
522
+ When changing scheduled-time logic, keep `_norm_sched` / `_sched_level` / `_sched_anchor` / `_sched_sort_key` / `_sched_display` / `cmd_add` / `cmd_defer` / `_import_node` / `_validate_fieldop` / `_exec_update` / `_node_line` in sync.
522
523
 
523
524
  ## 21. Log historical dates (for migration)
524
525
 
@@ -537,7 +538,7 @@ Goal: reproduce the day-dimension view of "all projects + progress on a given da
537
538
  - **Bucket = `work`/`personal` tag** → `Work` / `Personal` / `Other` (`_node_bucket`), order in `_BUCKET_ORDER`
538
539
  - **Secondary group `--by`** (`_sec_group` / `_sec_sort_key`):
539
540
  - `plan` (**`wl day` default** — closest to markdown day structure): scheduled that day (or migration-era `planned` tag) → `Planned`; everything else → `Unplanned`. (The old separate `Unplanned (untagged)` bucket was merged into `Unplanned` — now that planned/unplanned derives from sched, the tag distinction has no value and the label misled.)
540
- - `project`: project ancestor (`_node_project` picks the `kind=project` ancestor). `wl logs --group day` still defaults to `project`
541
+ - `project`: project ancestor (`_node_project` picks the project ancestor, `type.para=project`). `wl logs --group day` still defaults to `project`
541
542
  - `priority`: `A/B/C → P0/P1/P2`, no priority → `—` (defaults to unplanned but flagged unconfirmed). ⚠️ **Planned/unplanned is fundamentally per-day (per-log)** (a task may be planned today, unplanned tomorrow); hanging the mark on the task itself is an approximation after merging migration. Precise modeling would push the mark down to log rows (schema not yet done, decision pending).
542
543
  - **`wl logs --group day [--by ...]`** reuses `_render_day_group`: groups by date header, each day has the same structure as `wl day`
543
544
  - **`wl logs` default time window**: when no `--id` / `--date` / `--since`, only the last `--days` (default 7) — prevents flooding (works even when data grows); `--since` / `--until` / `--date` explicitly override
@@ -38,26 +38,26 @@ worklog 刻意保持**少依赖、低抽象**:运行时只有 **stdlib + `rich
38
38
  - 动词在前:`wl <verb> [args] [--flags]`
39
39
  - 子命令名用全小写单词:`add` / `done` / `tree` / `projects` / `changes` / `summary` / `focus` …
40
40
  - 操作单个节点的命令第一个位置参数是 `id`(int):`wl show 42` / `wl done 42` / `wl log 42 "..."`
41
- - 长 flag 用 `--xxx`,短 flag 仅给最高频的(`-k` kind / `-p` priority / `-t` tag)
41
+ - 长 flag 用 `--xxx`,短 flag 仅给最高频的(`-p` priority / `-t` tag)
42
42
  - 节点不存在一律 `sys.exit(f"✗ ... #{id} not found")`,退出码非 0
43
43
 
44
44
  ## 2. 数据模型(`src/worklog/migrations/`)
45
45
 
46
- 单 `node` 表承载一切,`kind` 字段区分类型,`parent_id` 自引用建树。schema 以编号 SQL migrations 的形式发布,放在 `src/worklog/migrations/NNNN_*.sql`,`PRAGMA user_version` 记录最高已应用版本。`ensure_db()` 每条命令都自动 apply pending migrations,显式形式是 `wl migrate`。初始版本见 `src/worklog/migrations/0001_initial_schema.sql`,整体概览见 `README.md`。
46
+ 单 `node` 表承载一切,分类由正交的 `type.*` prop 命名空间承担(没有专门的列),`parent_id` 自引用建树。schema 以编号 SQL migrations 的形式发布,放在 `src/worklog/migrations/NNNN_*.sql`,`PRAGMA user_version` 记录最高已应用版本。`ensure_db()` 每条命令都自动 apply pending migrations,显式形式是 `wl migrate`。初始版本见 `src/worklog/migrations/0001_initial_schema.sql`,整体概览见 `README.md`。
47
47
 
48
48
  > **migration 编写规则**:runner 把每个文件包进一个 `BEGIN/COMMIT`(中途失败整文件回滚),所以 migration 文件本身**不要**写 `BEGIN`/`COMMIT`。
49
49
 
50
- - **kind 取值**:`lifetime / decade / year / quarter / month / week / day / area / project / task / meetlog / habit / signal`(可扩展,但加新 kind 要想清楚它在 tree / projects / summary 里怎么归类)
50
+ - **分类取值**(正交的 `type.*` props,没有单列):`type.para` = PARA 角色 `area / project / task`;`type.date` = 时间层级 `lifetime / decade / year / quarter / month / week / day`;`type.habit`;`type.meetlog`;自定义 `type.<x>`(可扩展,但加新 type 要想清楚它在 tree / projects / summary 里怎么归类)。一个 node 可同时持有多个;需要单个代表 token 时由 `node_type` / `node_type_from_props` 派生(优先级 para > date > habit > meetlog > custom > task)
51
51
  - **status 只在 task / habit / meetlog 类用**;时间层级类(year/month/...)跟 project 类 status 留 NULL
52
52
  - 表:`node / tag / log / metric / clock / prop / link / sched / date_meta` + 派生 view `v_node_path`
53
53
  - **`node → log → metric` 主干**(log 为中心的核心):一个 `node` 挂多条 `log`;一条 `log`(带 `tag`——`note`/`goal`/`summary`/`metric`(载体)/…,NULL = 普通笔记)下挂 0..N 条 `metric`。`metric` 是结构化数据点(`tag` = 它是什么,如 `glucose`/`pullups`/`checkin`,或 `goal` = 一条 goal log 按优先级排序的目标 node id;外加 `value_num`/`value_text`/`unit`/`note`/`at`)。**`tag` 是三处统一的分类字段**——node(多值标签集)、log(角色,单值)、metric(种类,单值);同一个词、不同范围、SQL 不混。metric **必须挂在一条 log 上**(`metric.log_id` NOT NULL)——所以每个数据点都有 log 载体;`CHECK` 要求有值(纯标记如打卡存 `value_num=1`)。`metric.node_id` 是反范式冗余(免 join 查某 node 的数据点,无 FK;trigger 保证它始终等于载体 log 的 node)。CRUD 入口:`wl metric add/ls/edit/rm`(`add` 不带 `--on-log` 时建载体 log;无值标记存 `value_num=1`);`wl log`/`wl add` 的 `--metric` 和 `wl import` 的 `metrics` 可内联挂数据点。habit「今天做没做」= 当天有没有 `tag=checkin` 的 metric(`wl tick`/`wl checkin` 写),不再是「那天有没有 log」。
54
- - **保留 tag 的 log = 历史保留**:`goal`(前瞻,任意时间层级)和 `summary`(后顾日终)都是 `log.tag` 化的 log(最新一条=当前值,每次改追加一条),有自己的 `wl goal set/ls/rm <node>` 组(`--summary` 改写 summary),也由 bare `wl goal`/`wl recap`(今日自动)和 key-route 的 `wl set <node> goal|summary` 写。goal **每个层级同一个 `goal` tag**——node 的 kind(day/week/month/year)就是层级;旧的 week `overview`、month `top5` 已并入 goal(迁移 0010)。一条 goal 可带结构化目标(写时显式给的 node id,顺序=优先级,存成 `goal` metric);`wl day`/`wl goal` 展示 goal + 编号带状态的目标列表 + `[done/total]`。(`prop` 回归只放真正静态的单值属性。)
54
+ - **保留 tag 的 log = 历史保留**:`goal`(前瞻,任意时间层级)和 `summary`(后顾日终)都是 `log.tag` 化的 log(最新一条=当前值,每次改追加一条),有自己的 `wl goal set/ls/rm <node>` 组(`--summary` 改写 summary),也由 bare `wl goal`/`wl recap`(今日自动)和 key-route 的 `wl set <node> goal|summary` 写。goal **每个层级同一个 `goal` tag**——node 的 type(day/week/month/year)就是层级;旧的 week `overview`、month `top5` 已并入 goal(迁移 0010)。一条 goal 可带结构化目标(写时显式给的 node id,顺序=优先级,存成 `goal` metric);`wl day`/`wl goal` 展示 goal + 编号带状态的目标列表 + `[done/total]`。(`prop` 回归只放真正静态的单值属性。)
55
55
  - **`clock` = 结构化计时**:`clock(node_id, start_at, end_at, elapsed_sec)`,由 `wl start`/`stop`/`spent`/`wait` 读写——替代旧的 `CLOCK_IN`/`CLOCK_OUT` log-body 约定;时长从 `elapsed_sec` 求和,不再从文本解析。
56
56
  - **两条并列树(都挂 lifetime 下)**(2026-05-29 起):
57
57
  - **责任领域线**:`lifetime → area → project → task`(PARA:area 是跨时间的责任领域,project 归 area,task 归 project)
58
58
  - **时间线**:`lifetime → year → quarter → month → week → day`(承载时间骨架 + 保留 tag 的 log:每个层级的 `goal`、day 的 `summary` 日终)
59
59
  - 小结(`summary` log)自带 `logged_at` = 写入时间。`wl day` 显示「(写于 …)」,若当天小结后还有普通笔记 log(`tag IS NULL`)就提示「⚠ 小结后又有 N 条变更, 建议重写 recap」。(替代旧的 `summary_at` prop。)
60
- - **project 不再挂 month**(旧设计曾挂月,已迁到 area)。日/月/周视图靠 **log 的 logged_at**(时间维度)跟 **kind/tag/祖先链**(领域维度)解耦:`wl day` 按 log 日期驱动、project 经祖先链解析、bucket 经 work/personal tag——所以 project 移到 area 下不影响任何按天/按项目视图
60
+ - **project 不再挂 month**(旧设计曾挂月,已迁到 area)。日/月/周视图靠 **log 的 logged_at**(时间维度)跟 **type/tag/祖先链**(领域维度)解耦:`wl day` 按 log 日期驱动、project 经祖先链解析、bucket 经 work/personal tag——所以 project 移到 area 下不影响任何按天/按项目视图
61
61
 
62
62
  ## 3. 状态机(#+TODO 风格)
63
63
 
@@ -146,7 +146,7 @@ DONE / DEFERRED / CANCELED (已了结)
146
146
 
147
147
  ### 9.1 `wl tree` 默认行为(2026-05-29 调整,防刷屏)
148
148
 
149
- 两条并列树全展开会上千行。**纯 `wl tree`(无 --root/--kind/--depth/--by)= 专用概览视图 `_print_default_tree`**:
149
+ 两条并列树全展开会上千行。**纯 `wl tree`(无 --root/--para/--depth/--by)= 专用概览视图 `_print_default_tree`**:
150
150
 
151
151
  - **时间线展开到今天**:年→季→月→周→今天(只走到今天的那条路径,不列别的月/周/日),今天的 day 节点下**只列当天有 log 的任务**(task/habit/meetlog,不展开 log)。这样既看到当月、又只聚焦今天。
152
152
  - **area 只列一层**:7 个 area 只出领域名,不展开项目。
@@ -155,7 +155,7 @@ DONE / DEFERRED / CANCELED (已了结)
155
155
 
156
156
  底层 `_print_tree`(--root/--depth 路径)规则:
157
157
  - **限深**:有 `--root` 默认 3,无 root(`--depth` 显式)按给的值;`_print_tree(max_depth)` 截断。
158
- - **时间节点按日期排序**(`_tree_children`):kind时间类按 `title`(日期)升序,其他按 优先级→id(否则按 id 会 W22 排 W18 前)。
158
+ - **时间节点按日期排序**(`_tree_children`):type时间层级按 `title`(日期)升序,其他按 优先级→id(否则按 id 会 W22 排 W18 前)。
159
159
  - **day 节点展开 = 当天活动**(`_print_day_activity`):area 化后 day 无真子节点,改展开"当天有 log 的任务 + 只含当天的 log"(log 驱动,跟 `wl day` 同源)。**task 不要挂 day(挂 project)**,day 内容靠 log 日期推。
160
160
 
161
161
  ## 10. 项目↔任务关联(`_project_members`)
@@ -229,10 +229,10 @@ dev ai sync strategy reflection reading family health morning_check slack_scan
229
229
  ```json
230
230
  {
231
231
  "add": [
232
- { "ref": "p1", "title": "...", "kind": "project", "priority": "A",
233
- "tags": [...], "props": {...}, "links": [...], "logs": [...],
232
+ { "ref": "p1", "title": "...", "priority": "A",
233
+ "tags": [...], "props": {"type.para": "project"}, "links": [...], "logs": [...],
234
234
  "children": [ {...嵌套子节点...} ] },
235
- { "title": "...", "kind": "task", "parent_ref": "p1" }
235
+ { "title": "...", "parent_ref": "p1" }
236
236
  ],
237
237
  "update": [
238
238
  { "id": 45, "status": "DONE", "parent": 6, "add_tags": [...], "remove_tags": [...], "add_logs": [...], "add_links": [...] }
@@ -242,6 +242,7 @@ dev ai sync strategy reflection reading family health morning_check slack_scan
242
242
 
243
243
  约定:
244
244
 
245
+ - **分类 = `props` 里的 `type.*` 键**:`{"type.para":"project"}`(或 `area`/`task`)、`{"type.date":"day"}`(week/month/…)、`{"type.habit":"true"}`、`{"type.meetlog":"true"}`;无 `type.*` 的裸节点就是普通 task。`type.date` 会按 title 自动补全 `date.period`/区间
245
246
  - **父子两种表达并存**:`children` 嵌套(父 id 自动传子,灌时间分层天然用)+ `ref`/`parent_ref`(同批次临时引用,扁平交叉用)。`ref` 必须先定义后引用
246
247
  - **status=DONE 自动写 closed_at**(add 跟 update 都是)
247
248
  - **update 可改字段**:`status`/`priority`/`title`/`scheduled_at`/`deadline_at`/`body`/`parent`(move, 校验存在) + `add_tags`/`remove_tags`/`add_logs`/`add_links`。⚠️ 没列在白名单的 key 被静默忽略——`parent` 曾漏在白名单导致 move 静默失败(2026-05-29 修),加字段务必同步白名单
@@ -260,8 +261,8 @@ dev ai sync strategy reflection reading family health morning_check slack_scan
260
261
 
261
262
  ### 18.1 新增 `+` / 删除 `-` / 锚 ` `:节点行
262
263
 
263
- `<前缀><缩进>[marker] [#pri] #id [kind] title :tags:`(marker 必有;`+` 无 #id,`-`/锚 必有 #id)。
264
- 富字段子行 `@log <文本>` / `@link <doc>` / `@prop k=v`(附到上一个 `+`/锚 节点)。
264
+ `<前缀><缩进>[marker] [#pri] #id title :tags:`(marker 必有;`+` 无 #id,`-`/锚 必有 #id)。
265
+ 富字段子行 `@log <文本>` / `@link <doc>` / `@prop k=v`(附到上一个 `+`/锚 节点)。分类用 `@prop type.*` 设置(如 `@prop type.para=project`),节点行上不再有分类 token。
265
266
  marker → status:`[ ]`TODO `[x]`DONE `[/]`DOING `[>]`LATER `[?]`WAIT `[-]`CANCELED。
266
267
 
267
268
  ```
@@ -340,7 +341,7 @@ marker → status:`[ ]`TODO `[x]`DONE `[/]`DOING `[>]`LATER `[?]`WAIT `[-]`CAN
340
341
  **没有名叫 `default` 的调色板**——`default` 曾是第 4 个独立配色,语义混乱(既不是 dark 也不是 light)。现在只有三个真实调色板 + 一个 `auto` 选择器:
341
342
 
342
343
  `THEMES` 字典:每个调色板把语义名映射到 rich style。语义名(不是颜色名),全集见 `_THEME_KEYS`:
343
- `done/doing/later/wait/todo/canceled`(状态)、`pri_a/pri_b/pri_c`(优先级)、`id/kind/tag/hit/header/meta/planned/clock`。
344
+ `done/doing/later/wait/todo/canceled`(状态)、`pri_a/pri_b/pri_c`(优先级)、`id/type/tag/hit/header/meta/planned/clock`。
344
345
  - `dark`:深色背景,`bright_*` 提对比度
345
346
  - `light`:浅色背景,深色饱和色(`green4`/`red3`/`dark_orange3`/`dark_cyan`/`purple`/`grey42`…),避免亮色/白色在白底糊掉
346
347
  - `mono`:全 `default`(rich 的 default 风格=终端默认前景,无色),给"想要 rich 布局但不要颜色"的场景
@@ -360,7 +361,7 @@ out(_c("✓", "done") + " " + _c(f"#{id}", "id") + " " + _c(title))
360
361
 
361
362
  - `_CONSOLE is None`(纯文本):`_c` 原样返回 `str(text)`,零开销
362
363
  - styled:`_c` **先 `rich.markup.escape` 内容**(防 title 里的 `[x]`/`[[doc]]` 被当 markup 吃掉),再包 `[style]…[/style]`;`style=None` 只 escape 不包色
363
- - **铁律:任何要进 `out()` 的片段,凡可能含 `[` `]`(marker `[x]` / 优先级 `[#A]` / kind `[day]` / wikilink `[[doc]]` / 时间戳 `[ts]`),必须经 `_c` 包一层**。直接把含方括号的裸串塞进 `out()` 会触发 rich MarkupError 或被静默吞掉
364
+ - **铁律:任何要进 `out()` 的片段,凡可能含 `[` `]`(marker `[x]` / 优先级 `[#A]` / type `[day]` / wikilink `[[doc]]` / 时间戳 `[ts]`),必须经 `_c` 包一层**。直接把含方括号的裸串塞进 `out()` 会触发 rich MarkupError 或被静默吞掉
364
365
 
365
366
  ### 19.4 `out()` 取代 `print()`
366
367
 
@@ -393,10 +394,10 @@ out(_c("✓", "done") + " " + _c(f"#{id}", "id") + " " + _c(title))
393
394
  - 规范格式直接校验(非法日期/月份/周号抛 `ValueError`,在 dry-run 即报,不写脏数据)
394
395
  - 相对词归一化:`今天/today`→日期、`明天/tomorrow`→日期、`下周/next-week`→`YYYY-Www`、`下月/next-month`→`YYYY-MM`、`下季/next-quarter`→`YYYY-Qn`、`以后/someday`→`someday`
395
396
  - 无法识别 → `ValueError` + 提示合法格式(不静默吞)
396
- - **粒度 `_sched_kind(s)`** / **排序 `_sched_sort_key(s)`**:排序键 = `(起始日 anchor, 粒度 rank)`;模糊值 anchor 到其时间段起点(`2026-06`→`2026-06-01`、`2026-Q3`→`2026-07-01`),`someday` anchor 远未来排最后;同 anchor 时粒度细的排前
397
+ - **粒度 `_sched_level(s)`** / **排序 `_sched_sort_key(s)`**:排序键 = `(起始日 anchor, 粒度 rank)`;模糊值 anchor 到其时间段起点(`2026-06`→`2026-06-01`、`2026-Q3`→`2026-07-01`),`someday` anchor 远未来排最后;同 anchor 时粒度细的排前
397
398
  - **显示 `_sched_display(s)`**:精确日去年只显 `MM-DD`,模糊值显原样(`2026-06`/`2026-Q3`/`someday`);`_node_line(sched=True)` 用 `@<display>`(planned 风格高亮;前缀 `@` 而非 emoji——📅 太扎眼,`@` 轻量且在 plain 无色模式下跟前文分隔),ls / day / summary 默认开 `sched=True`
398
399
 
399
- 加 / 改计划时间相关时,`_norm_sched` / `_sched_kind` / `_sched_anchor` / `_sched_sort_key` / `_sched_display` / `cmd_add` / `cmd_defer` / `_import_node` / `_validate_fieldop` / `_exec_update` / `_node_line` 跟本节同步。
400
+ 加 / 改计划时间相关时,`_norm_sched` / `_sched_level` / `_sched_anchor` / `_sched_sort_key` / `_sched_display` / `cmd_add` / `cmd_defer` / `_import_node` / `_validate_fieldop` / `_exec_update` / `_node_line` 跟本节同步。
400
401
 
401
402
  ## 22. 当天复现视图 `wl day` + `wl logs --group`(markdown 日结构)
402
403
 
@@ -406,7 +407,7 @@ out(_c("✓", "done") + " " + _c(f"#{id}", "id") + " " + _c(title))
406
407
  - **桶(bucket)= `work`/`personal` tag** → `工作`/`个人`/`其他`(`_node_bucket`),顺序 `_BUCKET_ORDER`
407
408
  - **次级分组 `--by`**(`_sec_group` / `_sec_sort_key`):
408
409
  - `plan`(**`wl day` 默认**,设计决策 2026-05-29——最贴近 markdown 日结构):当天有排期(或迁移期 `planned` tag)→ `计划内`;其余 → `计划外`。(原来单独的 `计划外(未标)` 一档已并入 `计划外`——现在计划内外靠 sched 推导,那个 tag 区分没价值且 `(未标)` 文案误导,会被当成"没打 work/personal tag"。)
409
- - `project`:项目祖先(`_node_project` 取 kind=project 的 ancestor)。`wl logs --group day` 仍默认 `project`
410
+ - `project`:项目祖先(`_node_project` 取项目祖先,`type.para=project`)。`wl logs --group day` 仍默认 `project`
410
411
  - `priority`:`A/B/C → P0/P1/P2`,无优先级 `—`(默认当计划外,但标注未显式确认,设计决策 2026-05-29)。⚠️ **计划内/计划外本质是 per-day(per-log)属性**(同一任务今天计划内、明天可能计划外),归并迁移后挂在任务上只是近似;要精确需把标记下沉到 log 行(schema 未做,留待决策)
411
412
  - **`wl logs --group day [--by ...]`** 复用 `_render_day_group`:先按日期 header 分组,每天内同 `wl day` 结构
412
413
  - **`wl logs` 默认时间窗**:无 `--id`/`--date`/`--since` 时只列**最近 `--days`(默认 7)天**,防全量刷屏(数据涨上来还能用);`--since`/`--until`/`--date` 显式覆盖
@@ -435,7 +436,7 @@ out(_c("✓", "done") + " " + _c(f"#{id}", "id") + " " + _c(title))
435
436
 
436
437
  2026-05-29 拍板(vault §八 4/5):
437
438
 
438
- - **wl day 顶部的 goal / summary**(2026-05-29 起为 prop,后演进,**以下为现状**):`goal`(前瞻,任意层级)和 `summary`(日终)是节点上保留 tag 的 **log**(历史保留,非 prop)——day 写 goal/summary,week/month 写 goal(同一个 `goal` tag,靠 node kind 区分层级;旧的 `overview`/`top5` 已并入 goal)。`wl goal`/`wl recap`/`wl goal set <node>` 写,goal 可带结构化目标 node id(`goal` metric,顺序=优先级)。`wl day` 顶部按 `> 🎯 / > 📝 Recap / > 📅 This week / > ⭐ This month` blockquote 渲染 + 编号带状态的目标列表 + `[done/total]`。
439
+ - **wl day 顶部的 goal / summary**(2026-05-29 起为 prop,后演进,**以下为现状**):`goal`(前瞻,任意层级)和 `summary`(日终)是节点上保留 tag 的 **log**(历史保留,非 prop)——day 写 goal/summary,week/month 写 goal(同一个 `goal` tag,靠 node type 区分层级;旧的 `overview`/`top5` 已并入 goal)。`wl goal`/`wl recap`/`wl goal set <node>` 写,goal 可带结构化目标 node id(`goal` metric,顺序=优先级)。`wl day` 顶部按 `> 🎯 / > 📝 Recap / > 📅 This week / > ⭐ This month` blockquote 渲染 + 编号带状态的目标列表 + `[done/total]`。
439
440
  - **日期上下文 → 专门 `date_meta` 表**:`(date PRIMARY KEY, label)`。`label` = 节日/休假/调休/节气(劳动节假期 / 调休上班 / 小满 / 年假 …)。**不依赖 day 节点**,可全年预导入假期表 + 自定义。
440
441
  - **周X 自动算**(`_cn_weekday`,不存储),`date_meta` 只存非自动信息。
441
442
  - `wl day` 头部 = `<date> <周X>[ · <label>]`(`_date_label` 查表)。day 节点标题统一存**纯日期** `YYYY-MM-DD`(周X/节日不进标题,避免与自动算/查表重复)。
@@ -519,7 +520,7 @@ total = max(clock_total, log_span)
519
520
  | 类别 | 参数(顶层全局 OR 子命令本地) | 描述 |
520
521
  |---|---|---|
521
522
  | **状态过滤** | `--show-canceled`(顶层全局, 默认隐) / `--all`(本地, ls/projects: 包括 DONE+CANCELED) / `--status STATUS`(本地, ls 已有) | 默认隐 DONE 和/或 CANCELED |
522
- | **类型过滤** | `--kind KIND`(本地, ls/tree/find 已有) | task/project/area/... |
523
+ | **类型过滤** | `--para {area,project,task}`(本地, ls/tree/find/day) 走 type.para 精确匹配; 其它分类用 `--prop type.<x>`(如 type.meetlog / type.date=day) | 分类由 type.* 派生 |
523
524
  | **优先级过滤** | `--priority A/B/C`(待加, ls/find/day) | 限 P0/P1/P2 |
524
525
  | **标签过滤** | `--tag TAG`(本地, ls 已有, AND filter) | 多 tag 逗号分隔 AND |
525
526
  | **时间范围** | `--since/--until/--week/--month/--date`(parent parser, changes/summary/logs/projects 已用) | 时间窗口 |
@@ -545,7 +546,7 @@ total = max(clock_total, log_span)
545
546
  | `--show-canceled` | 顶层全局 | 跨所有 list/tree/find 命令的状态过滤 |
546
547
  | `--color/--theme/--version` | 顶层全局 | 渲染配置 |
547
548
  | `--since/--until/--week/--month` | 子命令本地(经 window parent parser) | 只 changes/summary/logs/projects 用 |
548
- | `--limit/--top/--by/--kind/--tag/--parent/--all` | 子命令本地 | 不一定每个命令都适用 |
549
+ | `--limit/--top/--by/--para/--tag/--parent/--all` | 子命令本地 | 不一定每个命令都适用 |
549
550
 
550
551
  **冲突处理规则**:
551
552
 
@@ -650,7 +651,7 @@ log 写错有两条出路:
650
651
  核心反模式: AI 用 `wl ls` / `wl logs --id N` 列全量找 1-2 内容; 浪费 token + 反复试.
651
652
 
652
653
  落地动作:
653
- - `wl ls` 无任何过滤参数时顶部加一行 hint 引导用 `wl find` / `--parent` / `--kind` / `wl day` 等精准入口 (仍列出, 不阻断)
654
+ - `wl ls` 无任何过滤参数时顶部加一行 hint 引导用 `wl find` / `--parent` / `--para` / `wl day` 等精准入口 (仍列出, 不阻断)
654
655
  - `wl logs --id N --tail K` 单 task 模式 tail 也生效 (之前仅 `--by-task` 配合)
655
656
  - vault 文档维护"已识别浪费场景" Audit 清单, 撞到一个反模式就修一个, 持续迭代
656
657
 
@@ -724,7 +725,7 @@ epilog = 使用场景 (3 条) + 跟 wl day 的区别 + 紧凑模式 / full mod
724
725
 
725
726
  ```
726
727
  help = "list nodes (默认限 20 条, 参考 shell ls -t / -S / -r 各种维度)"
727
- epilog = 10 条常用示例 (--parent / --kind / --tag / --unscheduled / --sort / --recent / --ids / --status / --all)
728
+ epilog = 10 条常用示例 (--parent / --para / --tag / --unscheduled / --sort / --recent / --ids / --status / --all)
728
729
  ```
729
730
 
730
731
  ### 信息密度 vs 简洁的权衡
@@ -0,0 +1,33 @@
1
+ # Local dev/test image — run the full test suite (and a dev wl) inside a Linux container,
2
+ # physically isolated from the real database.
3
+ #
4
+ # Why: a dev build of wl can connect to the real DB by default (no --db) and mutate it. CLI-flag
5
+ # isolation and "don't touch the real DB" conventions are easy to bypass by accident. The robust
6
+ # fix is physical isolation: run dev work and tests only inside this container.
7
+ #
8
+ # Isolation guarantees:
9
+ # - the container has its own HOME (/root) and filesystem; the host data dir does not exist in
10
+ # it and is never mounted in
11
+ # - even running `wl ...` (without --db) inside the container only creates a DB at a
12
+ # container-internal path, never touching host data
13
+ # - tests use pytest's temporary fixture DB (isolated by construction)
14
+ # - only the project code is bind-mounted to /app at run time; data dirs are never mounted
15
+ #
16
+ # Lighter than the production image: plain python + deps, fast to build.
17
+ FROM python:3.11-slim
18
+
19
+ RUN pip install --no-cache-dir uv
20
+
21
+ # Keep the venv outside /app (at /opt/venv) so bind-mounting the project root to /app at run time
22
+ # does not shadow it.
23
+ ENV UV_PROJECT_ENVIRONMENT=/opt/venv
24
+
25
+ # Build stage installs deps only (incl. semantic + test extras), not the project itself — the code
26
+ # is bind-mounted at run time and referenced via PYTHONPATH=/app/src, so edits take effect without
27
+ # rebuilding the image.
28
+ WORKDIR /build
29
+ COPY pyproject.toml uv.lock ./
30
+ RUN uv sync --all-extras --no-install-project
31
+
32
+ WORKDIR /app
33
+ CMD ["sh", "-c", "PYTHONPATH=/app/src /opt/venv/bin/python -m pytest"]
@@ -15,7 +15,7 @@ WORKLOG_DB_PATH := $(shell \
15
15
  if [ -n "$$WORKLOG_DB" ]; then echo "$$WORKLOG_DB"; \
16
16
  else echo "$${XDG_DATA_HOME:-$$HOME/.local/share}/worklog/worklog.db"; fi)
17
17
 
18
- .PHONY: help sync test test-v test-fast cov install uninstall reinstall push pull status commit demo clean reset setup
18
+ .PHONY: help sync test test-v test-fast cov docker-test docker-test-image install uninstall reinstall push pull status commit demo clean reset setup
19
19
 
20
20
  help: ## show this help
21
21
  @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
@@ -38,6 +38,16 @@ test-fast: ## run pytest only (no cov, no gate; quick dev feedback)
38
38
  cov: ## detailed coverage report (term-missing, includes 95% gate)
39
39
  @$(PYTEST)
40
40
 
41
+ # ── docker dev/test (physically isolated from the real DB — see Dockerfile.dev) ──
42
+
43
+ DEVTEST_IMG := worklog-devtest:latest
44
+
45
+ docker-test-image: ## build the isolated dev/test image
46
+ docker build -f Dockerfile.dev -t $(DEVTEST_IMG) .
47
+
48
+ docker-test: docker-test-image ## run the FULL suite inside the container (never mounts the real data dir)
49
+ docker run --rm -v "$(CURDIR)":/app -w /app $(DEVTEST_IMG)
50
+
41
51
  # ── install / uninstall ──
42
52
 
43
53
  install: sync ## install ~/bin/wl wrapper pointing into the project .venv
@@ -74,14 +84,14 @@ demo: ## populate a fresh demo DB with a sample tree (REFUSES to
74
84
  exit 1; \
75
85
  fi
76
86
  @wl init
77
- @wl add "Lifetime" -k lifetime
78
- @wl add "2026" -k year --parent 1
79
- @wl add "2026-Q2" -k quarter --parent 2
80
- @wl add "2026-05" -k month --parent 3
81
- @wl add "2026-W21" -k week --parent 4
82
- @wl add "2026-05-18 Mon" -k day --parent 5
83
- @wl add "Dev tooling" -k project -p A -t work --parent 4
84
- @wl add "Dev tooling — strategy pivot" -k task -p A -t work,unplanned,P0,dev_tooling --parent 6
87
+ @wl add "Lifetime" --prop type.date=lifetime
88
+ @wl add "2026" --prop type.date=year --parent 1
89
+ @wl add "2026-Q2" --prop type.date=quarter --parent 2
90
+ @wl add "2026-05" --prop type.date=month --parent 3
91
+ @wl add "2026-W21" --prop type.date=week --parent 4
92
+ @wl add "2026-05-18" --prop type.date=day --parent 5
93
+ @wl add "Dev tooling" --para project -p A -t work --parent 4
94
+ @wl add "Dev tooling — strategy pivot" -p A -t work,unplanned,P0,dev_tooling --parent 6
85
95
  @wl log 8 "2026-05-18 17:18 strategy pivot decided"
86
96
  @wl log 8 "2026-05-19 09:42 break down requirements: export_for_ai"
87
97
  @wl log 8 "2026-05-20 14:55 path B working end-to-end, owners 6/6 -> 7/7"