dsh-task-ledger 0.0.96 → 0.0.98

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 (37) hide show
  1. package/dist/client.js +3618 -116
  2. package/dist/client.js.map +1 -1
  3. package/dist/host/repositories/ledger.repository.d.ts +8 -2
  4. package/dist/host/repositories/ledger.repository.js +4 -1
  5. package/dist/host/repositories/records.d.ts +10 -2
  6. package/dist/host/repositories/records.js +11 -2
  7. package/dist/host/routes.js +1 -0
  8. package/dist/host/services/dates.d.ts +7 -2
  9. package/dist/host/services/dates.js +29 -6
  10. package/dist/host/services/history.service.js +1 -1
  11. package/dist/host/services/trend.service.d.ts +4 -1
  12. package/dist/host/services/trend.service.js +9 -1
  13. package/package.json +4 -1
  14. package/skill/README.md +2 -2
  15. package/skill/SKILL.md +43 -13
  16. package/skill/assets/worklog.md +5 -2
  17. package/skill/package.json +1 -1
  18. package/skill/references/state-model.md +6 -1
  19. package/skill/references/troubleshooting.md +5 -5
  20. package/skill/scripts/ledgerkit/actions/claim.py +7 -2
  21. package/skill/scripts/ledgerkit/actions/close_task.py +8 -0
  22. package/skill/scripts/ledgerkit/actions/handoff.py +5 -2
  23. package/skill/scripts/ledgerkit/actions/list_tasks.py +4 -1
  24. package/skill/scripts/ledgerkit/actions/log_progress.py +6 -2
  25. package/skill/scripts/ledgerkit/actions/new_task.py +7 -1
  26. package/skill/scripts/ledgerkit/actions/query_history.py +3 -1
  27. package/skill/scripts/ledgerkit/actions/release.py +6 -2
  28. package/skill/scripts/ledgerkit/actions/result.py +2 -0
  29. package/skill/scripts/ledgerkit/actions/show_task.py +32 -4
  30. package/skill/scripts/ledgerkit/actions/validate.py +6 -49
  31. package/skill/scripts/ledgerkit/core/identity.py +3 -3
  32. package/skill/scripts/ledgerkit/core/labels.py +16 -4
  33. package/skill/scripts/ledgerkit/core/time.py +2 -1
  34. package/skill/scripts/ledgerkit/core/worklog_md.py +216 -31
  35. package/skill/scripts/ledgerkit/shell/cli.py +24 -2
  36. package/skill/scripts/ledgerkit/shell/render.py +48 -4
  37. package/skill/scripts/ledgerkit/store/worklog_store.py +17 -5
@@ -18,6 +18,7 @@ from ..actions.result import (
18
18
  ShowResult,
19
19
  ValidationReport,
20
20
  )
21
+ from ..core.identity import Identity
21
22
  from ..core.time import fmt_local
22
23
 
23
24
 
@@ -51,6 +52,23 @@ def _json(data: Any) -> None:
51
52
  sys.stdout.write(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
52
53
 
53
54
 
55
+ #: 任务 / 历史行里要转本机时区的时间字段(存盘一律 UTC,出门一律本机时区)。
56
+ TIME_FIELDS = ("created", "started", "updated", "finished")
57
+
58
+
59
+ def localize_times(data: dict) -> dict:
60
+ """复制一份,把上面四个时间字段转成本机时区;其余字段原样。
61
+
62
+ **只动这四个**:任务号里嵌的时间戳、过程文档正文里的时间都不动(原文照旧)。
63
+ """
64
+ out = dict(data)
65
+ for key in TIME_FIELDS:
66
+ value = out.get(key)
67
+ if value:
68
+ out[key] = fmt_local(value)
69
+ return out
70
+
71
+
54
72
  def result(res: Result, *, as_json: bool) -> None:
55
73
  """写动作:一律人话(`--json` 不挂写动作,传进来也按人话打)。"""
56
74
  del as_json
@@ -71,7 +89,7 @@ def result(res: Result, *, as_json: bool) -> None:
71
89
 
72
90
  def list_view(res: ListResult, q: ListQuery | None, *, as_json: bool) -> None:
73
91
  if as_json:
74
- _json({tid: task for tid, task in res.rows.items()})
92
+ _json({tid: localize_times(task) for tid, task in res.rows.items()})
75
93
  return
76
94
  if not res.table:
77
95
  _out("没有符合条件的在办任务。")
@@ -84,11 +102,33 @@ def list_view(res: ListResult, q: ListQuery | None, *, as_json: bool) -> None:
84
102
  _err(line)
85
103
 
86
104
 
105
+ def whoami_view(identity: Identity | None, *, as_json: bool) -> None:
106
+ """本机身份:未初始化给 `uuid` / `name` 双 null(不是错误,调用方据此引导 init)。"""
107
+ if as_json:
108
+ _json(
109
+ {
110
+ "uuid": identity.uuid if identity else None,
111
+ "name": identity.name if identity else None,
112
+ }
113
+ )
114
+ return
115
+ if identity is None:
116
+ _out("这台机器还没有初始化身份。")
117
+ _out(
118
+ " 处理:请用户在自己的终端运行 "
119
+ "`progress.py init --repo <工作区根>`,由 ta 亲自选择/新建并确认。"
120
+ )
121
+ return
122
+ _out(f"@{identity.label()} {identity.uuid}")
123
+
124
+
87
125
  def show_view(res: ShowResult, *, as_json: bool) -> None:
88
126
  if as_json:
89
127
  _json(
90
128
  {
91
- "task": res.task,
129
+ "task": localize_times(res.task),
130
+ # 文档自述还是回退查在办:调用方据此知道字段来源(老文档 / 文档缺失)
131
+ "legacy": res.legacy,
92
132
  "worklog": {
93
133
  "path": res.worklog.path if res.worklog else None,
94
134
  "text": res.worklog.text if res.worklog else None,
@@ -125,11 +165,15 @@ def history_view(res: HistoryResult, q: HistoryQuery | None, *, as_json: bool) -
125
165
  _json({"count": res.count, "capped": res.capped})
126
166
  return
127
167
  if as_json:
168
+ filters = dict(res.filters)
169
+ for key in ("since", "until"):
170
+ if filters.get(key):
171
+ filters[key] = fmt_local(filters[key])
128
172
  _json(
129
173
  {
130
174
  "count": res.count,
131
- "filters": res.filters,
132
- "records": list(res.records),
175
+ "filters": filters,
176
+ "records": [localize_times(row) for row in res.records],
133
177
  "warnings": list(res.warnings),
134
178
  "capped": res.capped,
135
179
  }
@@ -23,7 +23,8 @@ from .atomic import ensure_dir, write_text_atomic
23
23
 
24
24
  FALLBACK_TEMPLATE = """# <id>:<title>
25
25
 
26
- > **创建**:<created>(@<creator>)
26
+ - **状态**:<status>
27
+ - **创建**:<created> · @<creator>
27
28
 
28
29
  ## 背景
29
30
 
@@ -131,9 +132,10 @@ class WorklogStore:
131
132
  creator: str,
132
133
  category: str | None = None,
133
134
  tags: tuple[str, ...] = (),
135
+ status: worklog_md.StatusLine | None = None,
134
136
  first_exec: str | None = None,
135
137
  ) -> Path:
136
- """一次写完整篇(含首条执行记录)。返回绝对路径。"""
138
+ """一次写完整篇(含状态行与首条执行记录)。返回绝对路径。"""
137
139
  rel = worklog_rel(tid, created)
138
140
  path = self.repo / to_repo_rel(rel)
139
141
  ensure_dir(path.parent)
@@ -144,6 +146,7 @@ class WorklogStore:
144
146
  creator=creator,
145
147
  category=category,
146
148
  tags=tags,
149
+ status=status,
147
150
  template=self.template(),
148
151
  )
149
152
  if first_exec:
@@ -155,14 +158,23 @@ class WorklogStore:
155
158
  """写回「锚点编辑后的新全文」;不许用它替换前五节正文。"""
156
159
  write_text_atomic(path, text)
157
160
 
158
- def append_exec(self, path: Path, line: str) -> bool:
159
- """追加一条执行记录;写不进返回 False,**不抛**(调用方自己决定报不报)。"""
161
+ def append_exec(
162
+ self, path: Path, line: str, *, status: worklog_md.StatusLine | None = None
163
+ ) -> bool:
164
+ """追加一条执行记录,并在**同一次写盘**里顺带刷新头部状态行(`status` 给了才刷)。
165
+
166
+ 写不进返回 False,**不抛**(调用方自己决定报不报)。状态行与执行记录必须一起落地:
167
+ 分两写就会留下「记录写了、状态没刷」这种不报错的不一致。
168
+ """
160
169
  try:
161
170
  text = path.read_text(encoding="utf-8-sig")
162
171
  except (OSError, UnicodeDecodeError):
163
172
  return False
164
173
  try:
165
- write_text_atomic(path, worklog_md.append_exec_log(text, line))
174
+ new = worklog_md.append_exec_log(text, line)
175
+ if status is not None:
176
+ new = worklog_md.rewrite_status_line(new, status)
177
+ write_text_atomic(path, new)
166
178
  except Exception: # noqa: BLE001 —— 写不进是这条记录的失败,不该掀翻整条命令
167
179
  return False
168
180
  return True