pyworklog 0.4.0__tar.gz → 0.5.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.
- {pyworklog-0.4.0 → pyworklog-0.5.0}/CHANGELOG.md +28 -7
- {pyworklog-0.4.0 → pyworklog-0.5.0}/DESIGN.md +10 -1
- {pyworklog-0.4.0 → pyworklog-0.5.0}/DESIGN.zh.md +10 -1
- {pyworklog-0.4.0 → pyworklog-0.5.0}/PKG-INFO +1 -1
- {pyworklog-0.4.0 → pyworklog-0.5.0}/pyproject.toml +1 -1
- {pyworklog-0.4.0 → pyworklog-0.5.0}/skills/worklog-cli/SKILL.md +2 -2
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/cli.py +40 -34
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/commands/bulk.py +47 -61
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/commands/meta.py +25 -27
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/commands/metric.py +38 -47
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/commands/query.py +99 -93
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/commands/state.py +92 -99
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/commands/views.py +197 -72
- pyworklog-0.5.0/src/worklog/db_table.py +174 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/helpers.py +17 -11
- pyworklog-0.5.0/src/worklog/migrations/0007_utc_timestamps.sql +46 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/queries.py +86 -38
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/render.py +2 -2
- pyworklog-0.5.0/src/worklog/timeutil.py +159 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/conftest.py +9 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_add.py +4 -4
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_brief.py +32 -32
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_checkin.py +5 -5
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_clock.py +5 -4
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_dateinfo.py +3 -3
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_day.py +85 -17
- pyworklog-0.5.0/tests/test_db_table.py +197 -0
- pyworklog-0.5.0/tests/test_filters.py +186 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_find.py +16 -16
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_focus.py +3 -3
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_import_apply.py +38 -38
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_log.py +13 -11
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_log_format.py +1 -1
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_logs.py +1 -1
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_meta.py +11 -11
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_metric.py +18 -15
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_migrations.py +44 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_projects.py +4 -4
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_render.py +16 -16
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_sched.py +27 -27
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_show.py +2 -2
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_state.py +38 -9
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_summary.py +1 -1
- pyworklog-0.5.0/tests/test_timeutil.py +136 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_tree.py +3 -1
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_ux.py +27 -27
- {pyworklog-0.4.0 → pyworklog-0.5.0}/uv.lock +1 -1
- {pyworklog-0.4.0 → pyworklog-0.5.0}/.github/workflows/release.yml +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/.github/workflows/test.yml +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/.gitignore +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/AGENTS.md +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/CONTRIBUTING.md +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/LICENSE +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/Makefile +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/README.md +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/README.zh.md +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/pytest.ini +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/__init__.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/commands/__init__.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/completion.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/db.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/migrations/0001_initial_schema.sql +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/migrations/0002_metric_and_log_tag.sql +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/migrations/0003_backfill_checkin_metrics.sql +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/migrations/0004_meta_props_to_typed_logs.sql +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/migrations/0005_clock_table.sql +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/migrations/0006_rename_log_type_to_tag.sql +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/src/worklog/xdg.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/__init__.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_aliases.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_cascade.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_changes.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_completion.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_config.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_init.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_link_set.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_ls.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_sample.py +0 -0
- {pyworklog-0.4.0 → pyworklog-0.5.0}/tests/test_xdg.py +0 -0
|
@@ -6,6 +6,19 @@ The autogenerated `Commits since vX.Y.Z` block on each GitHub Release captures e
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] — 2026-06-05
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- **Shared `--tag` / `--kind` / `--status` filter across `wl ls` / `tree` / `day` / `logs` / `agenda`** — one filter with the same meaning everywhere (wired through a parent parser, like the existing time-window flags), instead of each command rolling its own. `-t` / `--tag` is comma-separated **AND** (the node must carry every listed tag); `--kind` / `--status` are equality. So `wl day -t work` shows only the work bucket (empty buckets/groups aren't rendered, and the summary line counts the filtered set), `wl tree -t work` prunes the tree to matching nodes plus the ancestor paths that lead to them, and `ls` / `logs` / `agenda` filter identically. An explicit `--status DONE/CANCELED` overrides the default terminal-status hide. `-t` is now the short form on all five (matching `wl add -t`).
|
|
13
|
+
- **`wl day` header states the day's nature** — every day now reads at a glance: `workday` (Mon–Fri) or `weekend` (Sat/Sun) by default, refined to `holiday` / `leave` / `workday` when a `wl dateinfo` label says so, with the label appended for context — e.g. `2026-06-01 Mon · Children's Day holiday`, `2026-06-13 Sat · makeup workday`, `2026-05-21 Thu · workday (Grain Buds solar term)`. Previously the header showed a label only when one existed, so a bare day told you nothing about its working status.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Timestamps follow an `at`/`date` convention — precise instants stored UTC, literal dates kept local.** A column named `*_at` (or bare `at`: `created_at` / `closed_at` / `logged_at` / `metric.at` / `clock.start_at` / `end_at` / `sched.created_at`) is a precise instant, now stored as UTC and rendered in local time; a column holding a calendar *day* is named `*date` and kept verbatim as a local date. As part of this, `node.scheduled_at` → **`scheduled_date`** and `node.deadline_at` → **`deadline_date`** (they hold a planned/deadline day, plus fuzzy `@2026-06` pins — not instants). "Local" is the machine zone by default; `$WORKLOG_TZ` pins a fixed offset (e.g. `+08:00`) for reproducibility / overriding. Day-grouping (`wl day` / `summary` / `logs --date` / habit month-to-date) converts UTC→local before bucketing, so a log near local midnight lands on the right day. **Migration 0007** converts pre-existing rows from local (+08:00) to UTC (bare `YYYY-MM-DD` values left untouched) and renames the two date columns; new installs are unaffected.
|
|
17
|
+
- **Habit month-to-date label is English** — `(本月 N/M)` → `(this month N/M)` in `wl day` / `wl tree`, matching the rest of the output.
|
|
18
|
+
|
|
19
|
+
### Internal
|
|
20
|
+
- **A zero-dependency single-table CRUD layer (`db_table`).** All hand-written single-table SQL — inserts, updates, deletes, selects, dynamically-assembled `WHERE` / `SET`, and `INSERT OR IGNORE/REPLACE` — now goes through a small helper (`insert` / `update` / `delete` / `query` / `query_one` / `get` / `exists` / `count` / `clause`) with identifier validation and parameter binding throughout; SQL `datetime('now')` is gone (all timestamps flow through `timeutil`). No behavior change — a maintainability/safety refactor, codified as design goal **G3** (few deps: stdlib + `rich`; simple explicit SQL over query-builder DSLs; small helpers over an ORM). Genuinely-complex queries (display JOINs, `GROUP BY`, correlated subqueries) stay raw by design. The test fixtures were also de-Chinese'd to English (the relative-date input aliases like `明天` / `下周` remain — they're a feature).
|
|
21
|
+
|
|
9
22
|
## [0.4.0] — 2026-06-05
|
|
10
23
|
|
|
11
24
|
### Fixed
|
|
@@ -81,16 +94,24 @@ The autogenerated `Commits since vX.Y.Z` block on each GitHub Release captures e
|
|
|
81
94
|
Initial open-source release of `worklog-cli` (`wl`).
|
|
82
95
|
|
|
83
96
|
### Added
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
97
|
+
- **Single-table execution-system model** — one `node` table holds the whole hierarchy across 13 kinds (`lifetime` / `decade` / `year` / `quarter` / `month` / `week` / `day` / `area` / `project` / `task` / `habit` / `signal` / `meetlog`), sharing one id space and tree-linked via a `parent_id` self-reference. Six sibling tables hang off it: `log` (timestamped entries), `tag` (labels), `sched` (scheduling), `prop` (key-value attributes), `link` (vault-doc references), `date_meta`.
|
|
98
|
+
- **Seven-state task lifecycle** — `TODO` / `DOING` / `LATER` / `WAIT` / `DONE` / `DEFERRED` / `CANCELED`, a superset of the markdown `[ ]` / `[/]` / `[>]` / `[x]` four-state set that adds `LATER` (deferred to the future) vs. `WAIT` (blocked on someone). Plus `A` / `B` / `C` priority (rendered `[#A]` = P0) and auto-stamped `closed_at`.
|
|
99
|
+
- **37 subcommands** — create/edit (`add` / `set` / `link` / `init`), logging (`log` / `unlog` / `relog`), state transitions (`done` / `defer` / `start` / `stop` / `spent` / `active` / `wait` / `reopen` / `cancel` / `tick` / `checkin`), views (`show` / `ls` / `tree` / `focus` / `ancestors` / `descendants` / `projects` / `changes` / `summary` / `day`), meta fields (`goal` / `recap`), scheduling (`sched` / `dateinfo`), bulk I/O (`import` / `apply`), search (`find` / `logs`), and display (`themes` / `print-completion`). Most carry short aliases.
|
|
100
|
+
- **Scheduling with recurrence** — the `sched` table records both one-off dates (`on_date`) and recurrence rules (`rrule`: `daily`, `weekly:Mon,Wed,Fri`); a task scheduled for a given day counts as planned, derived from `sched` rather than a manual tag.
|
|
101
|
+
- **Time tracking** — `wl start` / `stop` / `spent` / `active` / `wait` record work intervals and report durations per node.
|
|
102
|
+
- **Project association via shared tags** — `wl projects` rolls a project's members up by structural children plus shared semantic tags, with a `GENERIC_TAGS` set (work / personal / planned / unplanned / P0–P2 / habit / meeting / …) excluded so generic dimension tags don't cross-link unrelated work.
|
|
103
|
+
- **Bulk loaders** — `wl import` (JSON) and `wl apply` (wl-diff format, symmetric with the rendered tree output) for scripted/AI-driven batch entry.
|
|
104
|
+
- **Compound flags on `wl add` / `done` / `cancel`** — create + log + close + timestamp + vault-link + schedule in one shot.
|
|
105
|
+
- **Date aliases** — `today` / `yesterday` / `tomorrow` / `day-before-yesterday` / … and Chinese equivalents resolve to concrete dates wherever a date is accepted.
|
|
106
|
+
- **Brief mode `-q`** on every command (skip log body / timeline / detail; ~50–90% character savings for AI consumers).
|
|
107
|
+
- **`rich`-based highlighting** with `dark` / `light` / `mono` themes, auto-detected via `$COLORFGBG` / OSC 11 terminal-background probe; manual override via `--theme` or `$WL_COLOR` / `$WL_THEME` / `$NO_COLOR`.
|
|
108
|
+
- **Shell completion** (fish / bash / zsh) generated at runtime by `wl print-completion`; loaded via init-load (the starship / direnv / zoxide pattern).
|
|
109
|
+
- **Vault integration** — `wl link` associates a node with one or more Obsidian vault docs (`[[doc]]`), kept in the `link` table for a two-way knowledge graph.
|
|
90
110
|
- Bilingual project documentation (English + Chinese for README / DESIGN / SKILL).
|
|
91
111
|
- MIT License.
|
|
92
112
|
|
|
93
|
-
[Unreleased]: https://github.com/xyb/worklog/compare/v0.
|
|
113
|
+
[Unreleased]: https://github.com/xyb/worklog/compare/v0.5.0...HEAD
|
|
114
|
+
[0.5.0]: https://github.com/xyb/worklog/compare/v0.4.0...v0.5.0
|
|
94
115
|
[0.4.0]: https://github.com/xyb/worklog/compare/v0.3.0...v0.4.0
|
|
95
116
|
[0.3.0]: https://github.com/xyb/worklog/compare/v0.2.0...v0.3.0
|
|
96
117
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## 0. Design goals (north star, override every specific convention)
|
|
9
9
|
|
|
10
|
-
These
|
|
10
|
+
These goals are foundational. Pass them before adding or changing any feature; when they conflict with a specific convention below, they win.
|
|
11
11
|
|
|
12
12
|
### G1 Structured-first (no string matching)
|
|
13
13
|
|
|
@@ -24,6 +24,15 @@ The tool must be simple enough to **use without reading the manual**, for both h
|
|
|
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
|
|
|
27
|
+
### G3 Few dependencies, simple logic, easy to maintain
|
|
28
|
+
|
|
29
|
+
worklog deliberately stays **dependency-light and low-abstraction**: runtime is **stdlib + `rich` only**, and new features prefer **zero new dependencies**. Logic stays **explicit and direct** — plain SQL over query-builder DSLs, small purpose-built helpers over frameworks/ORMs that hide what is happening. The bar: a maintainer (human or AI) can read any code path top-to-bottom without first learning a hidden layer.
|
|
30
|
+
|
|
31
|
+
- **Borrow the technique, not the library.** Prefer writing a tens-of-lines, zero-dependency helper over pulling in a package (e.g. a dict→`INSERT` helper in the spirit of sqlite-utils, *not* an ORM; a `field__op`→`WHERE` helper, *not* a query builder).
|
|
32
|
+
- **No ORM / no query builder.** Wrap only the uniform ~80 % (single-table CRUD + existence/count) in thin helpers that map transparently to one SQL statement; keep the complex ~20 % (JOIN / CTE / CASE / time-window) as explicit SQL. Don't reinvent SQL as a kwargs DSL.
|
|
33
|
+
- **Each table / module maintainable on its own.** Minimize forced coupling (hard FK cascades, cross-cutting magic, triggers that hide intent) so one part can change — or be migrated / synced — without rippling everywhere. Lean toward avoiding irreversible operations (prefer soft-delete / status over `DELETE`) where they create cross-table consistency burdens.
|
|
34
|
+
- When a "clever" abstraction conflicts with G3, choose the boringly-simple version. Fewer moving parts beats elegance.
|
|
35
|
+
|
|
27
36
|
## 1. Command style (todo.sh school)
|
|
28
37
|
|
|
29
38
|
- Verb first: `wl <verb> [args] [--flags]`
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
## 0. 设计目标(北极星,凌驾于一切具体约定之上)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
下面几条是地基。加任何功能、改任何设计前先过这几关;跟下文具体约定冲突时,以这几条为准。
|
|
11
11
|
|
|
12
12
|
### G1 结构化优先(不靠字符串匹配)
|
|
13
13
|
|
|
@@ -24,6 +24,15 @@ worklog 的整个职责就是把工作记录**结构化**,让信息能被精
|
|
|
24
24
|
- 任何新设计自检三问:(1) 它新增了几个概念?(2) 用它要不要逼人 / AI 做选择("用 A 还是 B?")?(3) 不看文档能不能猜对用法?任一条答得不好,就再简化。
|
|
25
25
|
- G1 和 G2 冲突时,找"既结构化又最少概念"的答案——别为了结构化就堆新表 / 新字段 / 新命令。
|
|
26
26
|
|
|
27
|
+
### G3 依赖少、逻辑简单、易维护
|
|
28
|
+
|
|
29
|
+
worklog 刻意保持**少依赖、低抽象**:运行时只有 **stdlib + `rich`**,新功能优先**零新依赖**。逻辑保持**显式直白**——宁可裸 SQL 也不上 query-builder DSL,宁可写小而专的 helper 也不引入藏掉细节的框架 / ORM。底线:维护者(人或 AI)能把任一条代码路径从头读到尾,不必先学一层隐藏机制。
|
|
30
|
+
|
|
31
|
+
- **借技法,不借库**:宁可写几十行零依赖 helper,也别拉一个包(例如照 sqlite-utils 思路写 dict→`INSERT` helper,而**不是** ORM;写 `field__op`→`WHERE` helper,而**不是** query builder)。
|
|
32
|
+
- **不上 ORM / 不上 query builder**:只把均匀的 ~80%(单表 CRUD + 存在/计数)包成**透明映射到一条 SQL**的薄 helper;复杂的 ~20%(JOIN / CTE / CASE / 时间窗)留显式 SQL。别把 SQL 重新发明成一套 kwargs DSL。
|
|
33
|
+
- **每张表 / 每个模块能各自维护**:尽量减少强制耦合(硬 FK 级联、跨切面魔法、藏意图的 trigger),让一处能改动 / 迁移 / 同步而不牵连全局。在会造成跨表一致性负担的地方,倾向**避免不可逆操作**(用软删除 / 状态标记替代 `DELETE`)。
|
|
34
|
+
- "聪明"的抽象跟 G3 冲突时,选无聊但简单的版本。零件少胜过优雅。
|
|
35
|
+
|
|
27
36
|
## 1. 命令风格(todo.sh 派)
|
|
28
37
|
|
|
29
38
|
- 动词在前:`wl <verb> [args] [--flags]`
|
|
@@ -32,7 +32,7 @@ Likewise, before adding a dev todo under a project, check that project's existin
|
|
|
32
32
|
| Schedule task to a date / repeat (drives "planned") | `wl sched <id> 2026-06-15` (also accepts `tomorrow` / `day-after-tomorrow`) / `--clear`. `--recur` supports period start / end: `daily` / `weekly:Mon,Fri` (also 1-7 / -1..-7) / `monthly:1` (month start) · `monthly:-1` (month end) / `quarterly:1-1` (quarter start) · `quarterly:-1` (quarter end) / `yearly:01-01` (year start) · `yearly:-1` (year end); `-1` always means period end. A task scheduled to a day shows up in `wl day` as "planned · not yet logged" even with no log |
|
|
33
33
|
| Meta info (end-of-day summary / Top5 / today's goal) | History-preserving typed logs (`log.tag`), not props: `wl goal "..."` (day) / `wl recap "..."` (day summary) / `wl set <month> top5 "..."` / `wl set <week> overview "..."`. Each write appends; the latest is current. `wl day` shows them at the top as a blockquote. Prefer `wl recap`/`wl goal` over `wl set` for day fields (read-back + stale-warning) |
|
|
34
34
|
| Date context (holidays / vacation / makeup days) | `wl dateinfo 2026-05-01 Labor-Day-holiday` / `wl dateinfo --import holidays.json` (`{"YYYY-MM-DD":"label"}`) / `wl dateinfo <date> --clear`. Weekday auto-computed; `wl day` header shows "date weekday · label" |
|
|
35
|
-
| Reproduce a day's progress (like markdown worklog) | `wl day [YYYY-MM-DD]` (log-date based: work/personal split → secondary group → task → indented logs + stats). **Default `--by plan`** (planned / unplanned
|
|
35
|
+
| Reproduce a day's progress (like markdown worklog) | `wl day [YYYY-MM-DD]` (log-date based: work/personal split → secondary group → task → indented logs + stats). **Default `--by plan`** (planned / unplanned). **Planned = has a `sched`/recur entry firing that day (source of truth, set via `wl sched`); everything else = unplanned.** The legacy `:planned:` tag is honored only as a transitional fallback — do NOT add it to new tasks, use `wl sched`. Switch dimensions with `--by project` / `--by priority` (P0/P1/P2) |
|
|
36
36
|
| List all active projects | `wl projects` |
|
|
37
37
|
| Tree view | `wl tree` (**default = overview: timeline expanded to today [year→quarter→month→week→today + today's tasks] + areas only listed as names, ~30 lines to avoid flooding**) / `wl tree --root <area>` (projects + tasks under that area) / `wl tree --root <week/month>` (per-day activity) / `wl tree --depth N` (fully expanded from lifetime) / `wl tree --by project/tag/direction` (switch dimension). Time nodes sorted by date; **day node expansion = tasks that have logs that day + only their logs from that day** |
|
|
38
38
|
| Time-range progress (weekly report input) | `wl changes --week 2026-W22` / `wl summary --week ... --by project/day` |
|
|
@@ -224,7 +224,7 @@ echo '{
|
|
|
224
224
|
|
|
225
225
|
```
|
|
226
226
|
#6 [day] 2026-05-29 Friday ← anchor: locate existing node as parent, don't modify
|
|
227
|
-
+ [x] [#A] morning-check
|
|
227
|
+
+ [x] [#A] morning-check ← add (indent = child), [x]=DONE; for planned use `wl sched`, not a :planned: tag
|
|
228
228
|
+ @log check key points
|
|
229
229
|
~ [x] #14 ← change #14 status (single-line shorthand)
|
|
230
230
|
~ #20 ← complex update: lock + field operations
|
|
@@ -183,6 +183,14 @@ def build_parser():
|
|
|
183
183
|
window.add_argument("--week", help="YYYY-Www (ISO week, overrides since/until)")
|
|
184
184
|
window.add_argument("--month", help="YYYY-MM (overrides since/until)")
|
|
185
185
|
|
|
186
|
+
# node-filter parent parser (reused by ls/tree/day/logs/agenda so every list/view
|
|
187
|
+
# command takes the SAME --tag/--kind/--status, with the same meaning — see
|
|
188
|
+
# make_node_filter). --tag is comma-separated AND.
|
|
189
|
+
filters = argparse.ArgumentParser(add_help=False)
|
|
190
|
+
filters.add_argument("-t", "--tag", help="comma-separated tags, AND filter (e.g. -t work)")
|
|
191
|
+
filters.add_argument("--kind", help="filter by kind (task/habit/meetlog/project/area/...)")
|
|
192
|
+
filters.add_argument("--status", help="filter by status (TODO/DOING/DONE/WAIT/LATER/CANCELED)")
|
|
193
|
+
|
|
186
194
|
_real_sub = p.add_subparsers(dest="cmd", required=False)
|
|
187
195
|
|
|
188
196
|
# wrap add_parser to inject user aliases (cross-shell uniform: wl d == wl day)
|
|
@@ -279,7 +287,7 @@ Differences from related commands:
|
|
|
279
287
|
a.add_argument("--proj", help="project (stored as prop)")
|
|
280
288
|
a.add_argument("--parent", type=int, help="parent node id")
|
|
281
289
|
a.add_argument("--status")
|
|
282
|
-
a.add_argument("--scheduled", help="(rough hint, writes node.
|
|
290
|
+
a.add_argument("--scheduled", help="(rough hint, writes node.scheduled_date) scheduled time: YYYY-MM-DD / YYYY-MM / YYYY-Www / YYYY-Qn / YYYY / someday / tomorrow / next-week / next-month / next-quarter")
|
|
283
291
|
a.add_argument("--sched", help="(precise, writes the sched table = visible as planned in `wl day` for that date) date: YYYY-MM-DD / today / yesterday / tomorrow / day-after-tomorrow")
|
|
284
292
|
a.add_argument("--deadline", help="deadline date YYYY-MM-DD")
|
|
285
293
|
a.add_argument("--body", help="optional body text")
|
|
@@ -337,7 +345,7 @@ Inverse of wl reopen (undo DONE back to TODO).""")
|
|
|
337
345
|
d.add_argument("--at", help="closed_at + log use this timestamp (HH:MM / YYYY-MM-DD [HH:MM[:SS]])")
|
|
338
346
|
|
|
339
347
|
df = sub.add_parser("defer",
|
|
340
|
-
help="defer a task to a future point (LATER +
|
|
348
|
+
help="defer a task to a future point (LATER + scheduled_date; fuzzy times supported)",
|
|
341
349
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
342
350
|
epilog="""\
|
|
343
351
|
Common examples:
|
|
@@ -347,7 +355,7 @@ Common examples:
|
|
|
347
355
|
wl defer 42 someday # no scheduled time
|
|
348
356
|
|
|
349
357
|
Differences from wl sched:
|
|
350
|
-
- wl defer -> status=LATER +
|
|
358
|
+
- wl defer -> status=LATER + scheduled_date field (rough hint, does NOT appear as "planned" in wl day on that day)
|
|
351
359
|
- wl sched -> writes to sched table (precise, appears as "planned" in wl day on that day)
|
|
352
360
|
To schedule it as planned for a specific day, use wl sched. defer is for "set aside, vaguely revisit later".""")
|
|
353
361
|
df.add_argument("id", type=int)
|
|
@@ -358,12 +366,12 @@ To schedule it as planned for a specific day, use wl sched. defer is for "set as
|
|
|
358
366
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
359
367
|
epilog="""\
|
|
360
368
|
Common examples:
|
|
361
|
-
wl start 42 # start timing now (
|
|
369
|
+
wl start 42 # start timing now (opens a clock interval)
|
|
362
370
|
wl start 42 43 # multiple tasks at once (parallel timers)
|
|
363
371
|
wl start 42 --at 09:00 # backfill 9am start (forgot to clock in)
|
|
364
372
|
wl start 42 --at 2026-05-30 14:30 # full ts
|
|
365
373
|
|
|
366
|
-
Related: close with wl stop <id>; see what's running via wl active; wl spent records a
|
|
374
|
+
Related: close with wl stop <id>; see what's running via wl active; wl spent records a clock interval from a duration.""")
|
|
367
375
|
s.add_argument("ids", type=int, nargs="+", help="node id(s)")
|
|
368
376
|
s.add_argument("--at", help="backfill start time: HH:MM (today) / YYYY-MM-DD / YYYY-MM-DD HH:MM[:SS]")
|
|
369
377
|
|
|
@@ -372,17 +380,17 @@ Related: close with wl stop <id>; see what's running via wl active; wl spent rec
|
|
|
372
380
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
373
381
|
epilog="""\
|
|
374
382
|
Common examples:
|
|
375
|
-
wl stop 42 # stop now,
|
|
383
|
+
wl stop 42 # stop now, close the interval (elapsed=Nmin)
|
|
376
384
|
wl stop 42 43 # batch stop
|
|
377
|
-
wl stop 42 --at 11:30 # backfill 11:30 end (must be later than
|
|
385
|
+
wl stop 42 --at 11:30 # backfill 11:30 end (must be later than the clock start)
|
|
378
386
|
wl stop 42 --at 2026-05-30 16:00 # full ts
|
|
379
387
|
|
|
380
|
-
Difference from wl spent: stop
|
|
388
|
+
Difference from wl spent: stop closes a prior open clock; spent creates a closed interval directly from a duration.""")
|
|
381
389
|
st.add_argument("ids", type=int, nargs="+", help="node id(s)")
|
|
382
|
-
st.add_argument("--at", help="backfill end time (must be later than
|
|
390
|
+
st.add_argument("--at", help="backfill end time (must be later than the clock start)")
|
|
383
391
|
|
|
384
392
|
sp = sub.add_parser("spent",
|
|
385
|
-
help="record a past time spent (build
|
|
393
|
+
help="record a past time spent (build a clock interval from a duration, good for retrospective entries)",
|
|
386
394
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
387
395
|
epilog="""\
|
|
388
396
|
Common examples:
|
|
@@ -392,14 +400,14 @@ Common examples:
|
|
|
392
400
|
wl spent 42 2h # 2 hours
|
|
393
401
|
wl spent 42 30m --at 14:30 # end at 14:30, backfill start at 14:00
|
|
394
402
|
|
|
395
|
-
Difference from wl start/stop: spent builds
|
|
403
|
+
Difference from wl start/stop: spent builds a closed clock interval from a duration in one step; no need to start first. Good for "forgot to clock, recording it after the fact".""")
|
|
396
404
|
sp.add_argument("id", type=int, help="node id")
|
|
397
405
|
sp.add_argument("duration", help="duration: 90 / 90m / 1h30m / 2h")
|
|
398
406
|
sp.add_argument("--at", help="end timestamp (default NOW); start = at - duration")
|
|
399
407
|
|
|
400
408
|
ac = sub.add_parser("active",
|
|
401
|
-
help="tasks running right now (open
|
|
402
|
-
description="List tasks that are timing right now (open
|
|
409
|
+
help="tasks running right now (open clock) + today's elapsed + latest log",
|
|
410
|
+
description="List tasks that are timing right now (an open clock interval). Shows current session elapsed, today's total, and the most recent log. Good for live focus check and finding tasks you forgot to stop.",
|
|
403
411
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
404
412
|
epilog="""\
|
|
405
413
|
Use cases:
|
|
@@ -409,14 +417,14 @@ Use cases:
|
|
|
409
417
|
|
|
410
418
|
Difference from wl day:
|
|
411
419
|
- wl day = full progress for the day (includes done / not-yet-started planned items), for end-of-day review
|
|
412
|
-
- wl active = what's timing right now (open
|
|
420
|
+
- wl active = what's timing right now (open clock), for live focus check
|
|
413
421
|
|
|
414
422
|
Output includes: current session elapsed + today's total (to decide stop or continue) + latest log (context).
|
|
415
423
|
Brief mode -q: id + elapsed only. Full log body: --log-format full.""")
|
|
416
424
|
# ac has no other flags but we keep the variable for future args (e.g. --since to look at past activity)
|
|
417
425
|
|
|
418
426
|
wa = sub.add_parser("wait",
|
|
419
|
-
help="mark WAIT (blocked on others / external input); auto-closes
|
|
427
|
+
help="mark WAIT (blocked on others / external input); auto-closes the clock; multiple ids",
|
|
420
428
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
421
429
|
epilog="""\
|
|
422
430
|
Common examples:
|
|
@@ -424,7 +432,7 @@ Common examples:
|
|
|
424
432
|
wl wait 42 --note "waiting on review" # add a log explaining what we're waiting on
|
|
425
433
|
wl wait 42 43 --note "waiting on approval" # batch
|
|
426
434
|
|
|
427
|
-
Note: marking WAIT auto-closes any open
|
|
435
|
+
Note: marking WAIT auto-closes any open clock (WAIT = suspended, no longer timing). Use wl reopen to revert to TODO.""")
|
|
428
436
|
wa.add_argument("ids", type=int, nargs="+", help="node id(s)")
|
|
429
437
|
wa.add_argument("--note", help="add a log explaining what you're waiting on")
|
|
430
438
|
|
|
@@ -537,7 +545,8 @@ Differences from related commands:
|
|
|
537
545
|
sh.add_argument("--all-timelines", action="store_true",
|
|
538
546
|
help="full timeline, no elision")
|
|
539
547
|
|
|
540
|
-
ls = sub.add_parser("ls",
|
|
548
|
+
ls = sub.add_parser("ls", parents=[filters],
|
|
549
|
+
help="list nodes (default limit 20; see shell ls -t / -S / -r-style dimensions)",
|
|
541
550
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
542
551
|
epilog="""\
|
|
543
552
|
Common examples (precise queries, shell-ls multi-dimensional):
|
|
@@ -553,9 +562,6 @@ Common examples (precise queries, shell-ls multi-dimensional):
|
|
|
553
562
|
wl ls --all remove the 20-row limit + include DONE/CANCELED
|
|
554
563
|
|
|
555
564
|
See also: wl find <q> / wl day / wl active / wl projects (each has a dedicated entry point sharper than ls)""")
|
|
556
|
-
ls.add_argument("--kind", help="filter by kind (task/habit/meetlog/project/area/...)")
|
|
557
|
-
ls.add_argument("--status", help="filter by status (TODO/DOING/DONE/WAIT/LATER/CANCELED)")
|
|
558
|
-
ls.add_argument("--tag", help="comma-separated tags, AND filter")
|
|
559
565
|
ls.add_argument("--parent", type=int, help="only direct children of this node")
|
|
560
566
|
ls.add_argument("--all", action="store_true", help="include DONE/CANCELED + remove the limit cap")
|
|
561
567
|
ls.add_argument("--limit", type=int, metavar="N",
|
|
@@ -574,7 +580,7 @@ See also: wl find <q> / wl day / wl active / wl projects (each has a dedicated e
|
|
|
574
580
|
ls.add_argument("--ids", type=int, nargs="+", metavar="id",
|
|
575
581
|
help="list specific ids directly, skipping filters (like shell `ls file1 file2`)")
|
|
576
582
|
|
|
577
|
-
tr = sub.add_parser("tree",
|
|
583
|
+
tr = sub.add_parser("tree", parents=[filters],
|
|
578
584
|
help="tree view of nodes (default: timeline up to today + areas one level, ~30 rows)",
|
|
579
585
|
description="Tree view of nodes. Default: timeline expanded up to today (year -> quarter -> month -> week -> today + today's tasks) + areas one level, ~30 rows to avoid scrolling. Use --root <id> to drill into a node.",
|
|
580
586
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
@@ -593,7 +599,6 @@ Differences from related commands:
|
|
|
593
599
|
- wl day log-date-driven view of a single day (not tied to tree)
|
|
594
600
|
- wl projects list projects as cards (subtask counts, no tree expansion)
|
|
595
601
|
- wl ls --parent <N> flat list of direct children, no recursion""")
|
|
596
|
-
tr.add_argument("--kind")
|
|
597
602
|
tr.add_argument("--proj")
|
|
598
603
|
tr.add_argument("--root", type=int, help="start tree from this node id")
|
|
599
604
|
tr.add_argument("--by", choices=["project", "tag", "direction"], help="regroup by dimension (flat 2-level)")
|
|
@@ -632,7 +637,7 @@ Related: wl show is self + timeline only; wl ancestors/descendants only go one d
|
|
|
632
637
|
de.add_argument("id", type=int)
|
|
633
638
|
de.add_argument("--depth", type=int, help="max depth")
|
|
634
639
|
|
|
635
|
-
ag = sub.add_parser("agenda",
|
|
640
|
+
ag = sub.add_parser("agenda", parents=[filters],
|
|
636
641
|
help="cross-time-range scheduling overview: everything scheduled in [start, end]",
|
|
637
642
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
638
643
|
epilog="""\
|
|
@@ -714,15 +719,16 @@ Dedup: by default a task appearing in multiple projects (via parent + shared tag
|
|
|
714
719
|
sm.add_argument("--no-dedup", action="store_true",
|
|
715
720
|
help="no dedup: a task across multiple projects is repeated in each bucket (old behavior)")
|
|
716
721
|
|
|
717
|
-
dy = sub.add_parser("day",
|
|
722
|
+
dy = sub.add_parser("day", parents=[filters],
|
|
718
723
|
help="full view of a day (default today): bucket -> project/plan -> task -> log",
|
|
719
|
-
description="Full view of one day: work/personal/other -> (planned/unplanned/project/priority) -> task -> indented logs. Top shows end-of-day summary + today's goal + Top5 (if set). Defaults to log-date-driven (works for past days too).",
|
|
724
|
+
description="Full view of one day: work/personal/other -> (planned/unplanned/project/priority) -> task -> indented logs. The header states the day's nature (workday / weekend, refined to holiday / leave / makeup by a `wl dateinfo` label). Top shows end-of-day summary + today's goal + Top5 (if set). Defaults to log-date-driven (works for past days too).",
|
|
720
725
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
721
726
|
epilog="""\
|
|
722
727
|
Common examples:
|
|
723
728
|
wl day # today
|
|
724
729
|
wl day 2026-05-30 # historical day
|
|
725
730
|
wl day yesterday # short form (yesterday / day-before-yesterday / tomorrow / day-after-tomorrow)
|
|
731
|
+
wl day -t work # only work items (filter; -t/--tag, AND); -t personal for personal
|
|
726
732
|
wl day --by project # change grouping (default plan: planned/unplanned)
|
|
727
733
|
wl day --by priority # group by P0/P1/P2
|
|
728
734
|
wl day --log-tail 1 # logs default to last 3, narrow to 1
|
|
@@ -770,7 +776,7 @@ Common examples:
|
|
|
770
776
|
wl recap --date yesterday # read yesterday's recap
|
|
771
777
|
|
|
772
778
|
wl day shows "Recap: ... (written at MM-DD HH:MM)" at the top;
|
|
773
|
-
if there are new
|
|
779
|
+
if there are new logs after recap, wl day shows "⚠ N changes after recap, consider rewriting".
|
|
774
780
|
Using wl set <day_id> summary "..." directly does not stamp the timestamp; not recommended.""")
|
|
775
781
|
rc.add_argument("text", nargs="?", help="no arg = read; with text = write")
|
|
776
782
|
rc.add_argument("--date", help="target day (YYYY-MM-DD / today / yesterday / 昨天 ...); default today")
|
|
@@ -804,20 +810,20 @@ Common examples:
|
|
|
804
810
|
wl unlog #L282 # exact delete by log id
|
|
805
811
|
wl unlog L282 # same (# optional)
|
|
806
812
|
wl unlog 282 # same (plain number)
|
|
807
|
-
wl unlog --node 39 # delete the latest
|
|
813
|
+
wl unlog --node 39 # delete the latest log for #39 today
|
|
808
814
|
wl unlog --node 39 --date yesterday # latest log that day
|
|
809
|
-
wl unlog --node 39 --all # delete all
|
|
815
|
+
wl unlog --node 39 --all # delete all logs for #39 that day
|
|
810
816
|
|
|
811
817
|
Find a log id: wl show <node_id> or wl logs --id <node_id> displays #L<id> in the timeline.
|
|
812
|
-
|
|
818
|
+
Edit a mistyped log with wl relog #L<id> instead. (Timing lives in the clock table, not logs — fix a clock with wl stop --at.)""")
|
|
813
819
|
ul.add_argument("log_id", type=_log_id_arg, nargs="?",
|
|
814
820
|
help="log id (e.g. #L282 / L282 / 282; from wl show / wl logs timeline)")
|
|
815
|
-
ul.add_argument("--node", type=int, help="delete by node id (default: latest
|
|
821
|
+
ul.add_argument("--node", type=int, help="delete by node id (default: latest log today)")
|
|
816
822
|
ul.add_argument("--date", help="with --node: delete logs from that day (default today)")
|
|
817
|
-
ul.add_argument("--all", action="store_true", help="with --node: delete all
|
|
823
|
+
ul.add_argument("--all", action="store_true", help="with --node: delete all logs for that node that day")
|
|
818
824
|
|
|
819
825
|
rl = sub.add_parser("relog",
|
|
820
|
-
help="rewrite a log: new body / new time / editor
|
|
826
|
+
help="rewrite a log: new body / new time / editor",
|
|
821
827
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
822
828
|
epilog="""\
|
|
823
829
|
Common examples:
|
|
@@ -827,7 +833,7 @@ Common examples:
|
|
|
827
833
|
wl relog #L282 --at 2026-05-30 # only change date (keep time)
|
|
828
834
|
wl relog #L282 # no body/--at -> open $EDITOR
|
|
829
835
|
|
|
830
|
-
|
|
836
|
+
Timing lives in the clock table, not logs — to fix a clock interval use wl stop --at.
|
|
831
837
|
Cannot move a log across nodes (that's unlog + log).""")
|
|
832
838
|
rl.add_argument("log_id", type=_log_id_arg,
|
|
833
839
|
help="log id (#L282 / L282 / 282; from wl show / wl logs)")
|
|
@@ -1018,7 +1024,7 @@ Before writing a new task / log, run wl find to check if there's an existing nod
|
|
|
1018
1024
|
fd.add_argument("--limit", type=int, metavar="N", help="show only the first N (default 20; use 0 or --all for no cap)")
|
|
1019
1025
|
fd.add_argument("--all", action="store_true", help="no row limit")
|
|
1020
1026
|
|
|
1021
|
-
lg = sub.add_parser("logs", parents=[window],
|
|
1027
|
+
lg = sub.add_parser("logs", parents=[window, filters],
|
|
1022
1028
|
help="list log entries (default last 7 days; preset today/yesterday/week/recent)",
|
|
1023
1029
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1024
1030
|
epilog="""\
|
|
@@ -11,6 +11,8 @@ from datetime import datetime, timedelta
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from .. import render
|
|
14
|
+
from .. import db_table as _db
|
|
15
|
+
from .. import timeutil as _tu
|
|
14
16
|
from ..helpers import (
|
|
15
17
|
_apply_top_limit,
|
|
16
18
|
_fmt_dur,
|
|
@@ -181,27 +183,23 @@ def cmd_apply(args, con):
|
|
|
181
183
|
# recursive subtree delete: node self-ref is ON DELETE SET NULL, only deleting the parent would orphan children, so we must explicitly collect descendants
|
|
182
184
|
ids = [f["id"]] + _collect_descendants(con, f["id"])
|
|
183
185
|
for did in ids:
|
|
184
|
-
|
|
186
|
+
_db.delete(con, "node", id=did)
|
|
185
187
|
counts["delete"] += len(ids)
|
|
186
188
|
continue
|
|
187
189
|
# pfx == "+": add new node
|
|
188
190
|
parent_id = stack.get(depth - 1) if depth > 0 else None
|
|
189
191
|
status = _MARKER_STATUS.get(f.get("marker", " "), "TODO")
|
|
190
192
|
kind = f.get("kind", "task")
|
|
193
|
+
now = _tu.utc_now()
|
|
194
|
+
node_row = {
|
|
195
|
+
"parent_id": parent_id, "title": f["title"], "kind": kind,
|
|
196
|
+
"status": status, "priority": f.get("priority"), "created_at": now,
|
|
197
|
+
}
|
|
191
198
|
if status == "DONE":
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"VALUES (?,?,?,?,?, datetime('now','localtime'))",
|
|
195
|
-
(parent_id, f["title"], kind, status, f.get("priority")),
|
|
196
|
-
)
|
|
197
|
-
else:
|
|
198
|
-
cur = con.execute(
|
|
199
|
-
"INSERT INTO node (parent_id,title,kind,status,priority) VALUES (?,?,?,?,?)",
|
|
200
|
-
(parent_id, f["title"], kind, status, f.get("priority")),
|
|
201
|
-
)
|
|
202
|
-
nid = cur.lastrowid
|
|
199
|
+
node_row["closed_at"] = now
|
|
200
|
+
nid = _db.insert(con, "node", node_row)
|
|
203
201
|
for t in f.get("tags", []):
|
|
204
|
-
|
|
202
|
+
_db.insert(con, "tag", {"node_id": nid, "tag": t}, or_="ignore")
|
|
205
203
|
for kind_, val in o["subs"]:
|
|
206
204
|
_apply_sub(con, nid, kind_, val)
|
|
207
205
|
counts["add"] += 1
|
|
@@ -229,48 +227,40 @@ def _import_node(con, spec, parent_id, ref_map, dry, counters):
|
|
|
229
227
|
if spec["parent_ref"] not in ref_map:
|
|
230
228
|
raise ValueError(f"parent_ref '{spec['parent_ref']}' undefined (must appear before reference)")
|
|
231
229
|
pid = ref_map[spec["parent_ref"]]
|
|
232
|
-
closed_at = None
|
|
233
|
-
if status == "DONE":
|
|
234
|
-
closed_at = "datetime_now" # placeholder; the SQL below uses datetime('now','localtime')
|
|
235
|
-
|
|
236
230
|
if dry:
|
|
237
231
|
nid = f"<ref:{spec.get('ref', '?')}>"
|
|
238
232
|
counters["add"] += 1
|
|
239
233
|
else:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"VALUES (?,?,?,?,?,?,?,?)",
|
|
251
|
-
(pid, title, kind, status, spec.get("priority"), sched,
|
|
252
|
-
spec.get("deadline"), spec.get("body")),
|
|
253
|
-
)
|
|
254
|
-
nid = cur.lastrowid
|
|
234
|
+
now = _tu.utc_now()
|
|
235
|
+
node_row = {
|
|
236
|
+
"parent_id": pid, "title": title, "kind": kind, "status": status,
|
|
237
|
+
"priority": spec.get("priority"), "scheduled_date": sched,
|
|
238
|
+
"deadline_date": spec.get("deadline"), "body": spec.get("body"),
|
|
239
|
+
"created_at": now,
|
|
240
|
+
}
|
|
241
|
+
if status == "DONE":
|
|
242
|
+
node_row["closed_at"] = now
|
|
243
|
+
nid = _db.insert(con, "node", node_row)
|
|
255
244
|
counters["add"] += 1
|
|
256
245
|
for t in spec.get("tags", []):
|
|
257
|
-
|
|
246
|
+
_db.insert(con, "tag", {"node_id": nid, "tag": t}, or_="ignore")
|
|
258
247
|
for k, v in (spec.get("props") or {}).items():
|
|
259
248
|
_upsert_prop(con, nid, k, str(v))
|
|
260
249
|
for d in spec.get("links", []):
|
|
261
|
-
|
|
250
|
+
_db.insert(con, "link", {"node_id": nid, "vault_doc": d}, or_="ignore")
|
|
262
251
|
for entry in spec.get("logs", []):
|
|
263
252
|
_insert_log(con, nid, entry)
|
|
264
253
|
# a log entry may carry structured datapoints: {"body":..., "metrics":[{tag,value,unit}]}
|
|
265
254
|
if isinstance(entry, dict) and entry.get("metrics"):
|
|
266
255
|
log_id = con.execute("SELECT last_insert_rowid()").fetchone()[0]
|
|
267
|
-
log_at =
|
|
256
|
+
log_at = _db.get(con, "log", log_id)["logged_at"]
|
|
268
257
|
for mspec in entry["metrics"]:
|
|
269
258
|
import_metric(con, log_id, nid, mspec, default_at=log_at)
|
|
270
259
|
# node-level metrics → a dedicated carrier log (1 carrier → N datapoints, e.g. a CGM import)
|
|
271
260
|
if spec.get("metrics"):
|
|
272
|
-
|
|
273
|
-
|
|
261
|
+
log_id = _db.insert(con, "log", {
|
|
262
|
+
"node_id": nid, "logged_at": _tu.utc_now(), "body": "", "tag": _CARRIER_TYPE,
|
|
263
|
+
})
|
|
274
264
|
for mspec in spec["metrics"]:
|
|
275
265
|
import_metric(con, log_id, nid, mspec)
|
|
276
266
|
counters["metric"] = counters.get("metric", 0) + len(spec["metrics"])
|
|
@@ -299,24 +289,20 @@ def _import_update(con, spec, dry, counters):
|
|
|
299
289
|
return
|
|
300
290
|
if "parent" in spec and spec["parent"] is not None and not _node_exists(con, spec["parent"]):
|
|
301
291
|
raise ValueError(f"update #{nid}: parent #{spec['parent']} does not exist")
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
vals.append(spec[col])
|
|
307
|
-
if "parent" in spec: # move; parent_id column name differs from spec key, handled separately
|
|
308
|
-
fields.append("parent_id = ?")
|
|
309
|
-
vals.append(spec["parent"])
|
|
292
|
+
changes = {col: spec[col] for col in
|
|
293
|
+
("status", "priority", "title", "scheduled_date", "deadline_date", "body") if col in spec}
|
|
294
|
+
if "parent" in spec: # move; spec key 'parent' maps to the parent_id column
|
|
295
|
+
changes["parent_id"] = spec["parent"]
|
|
310
296
|
if spec.get("status") == "DONE" and "closed_at" not in spec:
|
|
311
|
-
|
|
312
|
-
if
|
|
313
|
-
|
|
297
|
+
changes["closed_at"] = _tu.utc_now()
|
|
298
|
+
if changes:
|
|
299
|
+
_db.update(con, "node", nid, changes)
|
|
314
300
|
for t in spec.get("add_tags", []):
|
|
315
|
-
|
|
301
|
+
_db.insert(con, "tag", {"node_id": nid, "tag": t}, or_="ignore")
|
|
316
302
|
for t in spec.get("remove_tags", []):
|
|
317
|
-
|
|
303
|
+
_db.delete(con, "tag", node_id=nid, tag=t)
|
|
318
304
|
for d in spec.get("add_links", []):
|
|
319
|
-
|
|
305
|
+
_db.insert(con, "link", {"node_id": nid, "vault_doc": d}, or_="ignore")
|
|
320
306
|
for entry in spec.get("add_logs", []):
|
|
321
307
|
_insert_log(con, nid, entry)
|
|
322
308
|
counters["update"] += 1
|
|
@@ -460,7 +446,7 @@ def _parse_wld(text):
|
|
|
460
446
|
|
|
461
447
|
_STATUSES = {"TODO", "DOING", "LATER", "WAIT", "DONE", "DEFERRED", "CANCELED"}
|
|
462
448
|
_SET_COL = {"status": "status", "priority": "priority", "title": "title",
|
|
463
|
-
"parent": "parent_id", "scheduled": "
|
|
449
|
+
"parent": "parent_id", "scheduled": "scheduled_date", "deadline": "deadline_date"}
|
|
464
450
|
|
|
465
451
|
def _validate_fieldop(con, lineno, action, field, value, errs):
|
|
466
452
|
if field == "status" and action == "set" and value not in _STATUSES:
|
|
@@ -485,7 +471,7 @@ def _exec_update(con, o):
|
|
|
485
471
|
if field in _SET_COL:
|
|
486
472
|
col = _SET_COL[field]
|
|
487
473
|
if action == "clear":
|
|
488
|
-
|
|
474
|
+
_db.update(con, "node", nid, {col: None})
|
|
489
475
|
else:
|
|
490
476
|
if field == "parent":
|
|
491
477
|
v = int(value)
|
|
@@ -493,27 +479,27 @@ def _exec_update(con, o):
|
|
|
493
479
|
v = _norm_sched(value)
|
|
494
480
|
else:
|
|
495
481
|
v = value
|
|
496
|
-
|
|
482
|
+
_db.update(con, "node", nid, {col: v})
|
|
497
483
|
if field == "status" and value == "DONE":
|
|
498
|
-
con.execute("UPDATE node SET closed_at =
|
|
484
|
+
con.execute("UPDATE node SET closed_at = ? WHERE id = ? AND closed_at IS NULL", (_tu.utc_now(), nid))
|
|
499
485
|
elif field == "tag":
|
|
500
486
|
if action == "add":
|
|
501
|
-
|
|
487
|
+
_db.insert(con, "tag", {"node_id": nid, "tag": value}, or_="ignore")
|
|
502
488
|
else:
|
|
503
|
-
|
|
489
|
+
_db.delete(con, "tag", node_id=nid, tag=value)
|
|
504
490
|
elif field == "log":
|
|
505
491
|
_insert_log(con, nid, value)
|
|
506
492
|
elif field == "link":
|
|
507
493
|
if action == "add":
|
|
508
|
-
|
|
494
|
+
_db.insert(con, "link", {"node_id": nid, "vault_doc": value}, or_="ignore")
|
|
509
495
|
else:
|
|
510
|
-
|
|
496
|
+
_db.delete(con, "link", node_id=nid, vault_doc=value)
|
|
511
497
|
elif field == "prop":
|
|
512
498
|
if action == "set":
|
|
513
499
|
k, v = value
|
|
514
500
|
_upsert_prop(con, nid, k, v)
|
|
515
501
|
else:
|
|
516
|
-
|
|
502
|
+
_db.delete(con, "prop", node_id=nid, key=value)
|
|
517
503
|
|
|
518
504
|
def _fieldop_desc(action, field, value):
|
|
519
505
|
if action == "clear":
|
|
@@ -530,7 +516,7 @@ def _apply_sub(con, nid, kind, val):
|
|
|
530
516
|
if kind == "log":
|
|
531
517
|
_insert_log(con, nid, val)
|
|
532
518
|
elif kind == "link":
|
|
533
|
-
|
|
519
|
+
_db.insert(con, "link", {"node_id": nid, "vault_doc": val}, or_="ignore")
|
|
534
520
|
elif kind == "prop":
|
|
535
521
|
if "=" in val:
|
|
536
522
|
k, v = val.split("=", 1)
|