dsh-task-ledger 0.0.101 → 0.0.102
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.
- package/dist/client.js +2 -2
- package/dist/host/repositories/runner.d.ts +1 -1
- package/dist/host/repositories/runner.js +10 -7
- package/dist/host/skill/registration.d.ts +2 -1
- package/dist/host/skill/registration.js +4 -3
- package/package.json +1 -1
- package/skill/README.md +4 -2
- package/skill/SKILL.md +5 -3
- package/skill/assets/worklog.md +1 -1
- package/skill/package.json +1 -1
- package/skill/references/init-identity.md +3 -3
- package/skill/references/log-note.md +1 -1
- package/skill/references/troubleshooting.md +1 -1
- package/skill/scripts/bin/ledger-darwin-arm64 +0 -0
- package/skill/scripts/bin/ledger-linux-amd64 +0 -0
- package/skill/scripts/bin/ledger-windows-amd64.exe +0 -0
- package/skill/scripts/progress +44 -0
- package/skill/scripts/ledgerkit/__init__.py +0 -0
- package/skill/scripts/ledgerkit/actions/__init__.py +0 -0
- package/skill/scripts/ledgerkit/actions/atom.py +0 -154
- package/skill/scripts/ledgerkit/actions/bootstrap.py +0 -37
- package/skill/scripts/ledgerkit/actions/claim.py +0 -88
- package/skill/scripts/ledgerkit/actions/close_task.py +0 -191
- package/skill/scripts/ledgerkit/actions/guard.py +0 -158
- package/skill/scripts/ledgerkit/actions/handoff.py +0 -93
- package/skill/scripts/ledgerkit/actions/init_identity.py +0 -90
- package/skill/scripts/ledgerkit/actions/list_tasks.py +0 -74
- package/skill/scripts/ledgerkit/actions/log_progress.py +0 -67
- package/skill/scripts/ledgerkit/actions/new_task.py +0 -129
- package/skill/scripts/ledgerkit/actions/query.py +0 -35
- package/skill/scripts/ledgerkit/actions/query_history.py +0 -220
- package/skill/scripts/ledgerkit/actions/registry.py +0 -82
- package/skill/scripts/ledgerkit/actions/relabel.py +0 -89
- package/skill/scripts/ledgerkit/actions/release.py +0 -76
- package/skill/scripts/ledgerkit/actions/result.py +0 -90
- package/skill/scripts/ledgerkit/actions/set_task.py +0 -71
- package/skill/scripts/ledgerkit/actions/show_task.py +0 -55
- package/skill/scripts/ledgerkit/actions/validate.py +0 -189
- package/skill/scripts/ledgerkit/core/__init__.py +0 -0
- package/skill/scripts/ledgerkit/core/history_md.py +0 -152
- package/skill/scripts/ledgerkit/core/identity.py +0 -54
- package/skill/scripts/ledgerkit/core/ids.py +0 -58
- package/skill/scripts/ledgerkit/core/labels.py +0 -89
- package/skill/scripts/ledgerkit/core/paths.py +0 -92
- package/skill/scripts/ledgerkit/core/roots.py +0 -74
- package/skill/scripts/ledgerkit/core/time.py +0 -103
- package/skill/scripts/ledgerkit/core/worklog_md.py +0 -435
- package/skill/scripts/ledgerkit/env.py +0 -94
- package/skill/scripts/ledgerkit/errors.py +0 -15
- package/skill/scripts/ledgerkit/notices.py +0 -28
- package/skill/scripts/ledgerkit/shell/__init__.py +0 -0
- package/skill/scripts/ledgerkit/shell/cli.py +0 -532
- package/skill/scripts/ledgerkit/shell/gitops.py +0 -668
- package/skill/scripts/ledgerkit/shell/merge.py +0 -266
- package/skill/scripts/ledgerkit/shell/presync.py +0 -23
- package/skill/scripts/ledgerkit/shell/render.py +0 -216
- package/skill/scripts/ledgerkit/shell/runner.py +0 -232
- package/skill/scripts/ledgerkit/store/__init__.py +0 -0
- package/skill/scripts/ledgerkit/store/atomic.py +0 -130
- package/skill/scripts/ledgerkit/store/dirty.py +0 -108
- package/skill/scripts/ledgerkit/store/history_store.py +0 -134
- package/skill/scripts/ledgerkit/store/json_store.py +0 -166
- package/skill/scripts/ledgerkit/store/locks.py +0 -64
- package/skill/scripts/ledgerkit/store/owners_index.py +0 -127
- package/skill/scripts/ledgerkit/store/task_store.py +0 -28
- package/skill/scripts/ledgerkit/store/worklog_store.py +0 -178
- package/skill/scripts/progress.py +0 -20
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
"""合并语义:push 被拒后 rebase 撞上的**台账冲突**怎么解。
|
|
2
|
-
|
|
3
|
-
只认台账自己的文件:`pool.json` / 分片 / `owners.json` 按**任务号**三路合并,
|
|
4
|
-
`worklog/` 与 `history/` 是追加式正文,两边各加的内容并在一起。业务代码一个字都不碰。
|
|
5
|
-
|
|
6
|
-
rebase 的 stage 语义与字面直觉相反(实测确认):`:1` 共同祖先、`:2` 上游(别人的)、
|
|
7
|
-
`:3` 本机正在重放的那个提交。本文档的 `ours` 一律指 `:2`、`theirs` 指 `:3`,与 git
|
|
8
|
-
自己在 rebase 里的叫法一致 —— 标反了会让人照着冲突标记把别人的版本留下来。
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import json
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any
|
|
16
|
-
|
|
17
|
-
from ..core.paths import (
|
|
18
|
-
HISTORY_DIR,
|
|
19
|
-
OWNERS_DIR,
|
|
20
|
-
OWNERS_REL,
|
|
21
|
-
POOL_REL,
|
|
22
|
-
WORKLOG_DIR,
|
|
23
|
-
to_ledger_rel,
|
|
24
|
-
)
|
|
25
|
-
from ..store.atomic import write_text_atomic
|
|
26
|
-
from ..store.json_store import dump_ledger_json
|
|
27
|
-
|
|
28
|
-
EXPAND_KEYS = ("tasks", "workers")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _load(text: str) -> dict[str, Any] | None:
|
|
32
|
-
if not text.strip():
|
|
33
|
-
return {}
|
|
34
|
-
try:
|
|
35
|
-
data = json.loads(text)
|
|
36
|
-
except json.JSONDecodeError:
|
|
37
|
-
return None
|
|
38
|
-
return data if isinstance(data, dict) else None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _coerce(value: str | dict | None) -> dict[str, Any] | None:
|
|
42
|
-
"""三方内容:git 给的是文本(`git show :N:rel`),测试与调用方也可以直接给 dict。"""
|
|
43
|
-
if value is None:
|
|
44
|
-
return None
|
|
45
|
-
if isinstance(value, dict):
|
|
46
|
-
return value
|
|
47
|
-
return _load(str(value))
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _rows_of(data: dict[str, Any] | None, key: str) -> dict[str, Any]:
|
|
51
|
-
rows = (data or {}).get(key)
|
|
52
|
-
return rows if isinstance(rows, dict) else {}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def is_ledger_json(rel: str) -> bool:
|
|
56
|
-
"""是不是按条目合并的 JSON 台账(`pool.json` / 分片 / `owners.json`)。"""
|
|
57
|
-
r = to_ledger_rel(rel)
|
|
58
|
-
return (
|
|
59
|
-
r == POOL_REL.as_posix()
|
|
60
|
-
or r == OWNERS_REL.as_posix()
|
|
61
|
-
or (r.startswith(OWNERS_DIR.as_posix() + "/") and r.endswith(".json"))
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def is_append_only(rel: str) -> bool:
|
|
66
|
-
"""是不是追加式正文(过程文档 / 历史):只加行,不改写别人的行。"""
|
|
67
|
-
r = to_ledger_rel(rel)
|
|
68
|
-
return r.startswith((WORKLOG_DIR.as_posix() + "/", HISTORY_DIR.as_posix() + "/"))
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def owns(rel: str) -> bool:
|
|
72
|
-
"""这份冲突文件归不归脚本管(业务文件一个字都不碰)。"""
|
|
73
|
-
return is_ledger_json(rel) or is_append_only(rel)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def expand_key_of(rel: str) -> str:
|
|
77
|
-
"""这份 JSON 台账按哪个字段做条目级合并。"""
|
|
78
|
-
return "workers" if to_ledger_rel(rel) == OWNERS_REL.as_posix() else "tasks"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def merge_task_dicts(
|
|
82
|
-
base: str | dict | None,
|
|
83
|
-
ours: str | dict | None,
|
|
84
|
-
theirs: str | dict | None,
|
|
85
|
-
) -> tuple[dict[str, Any], list[str]]:
|
|
86
|
-
"""按任务号做三方合并;返回 (合并结果, 两边都改过的任务号)。
|
|
87
|
-
|
|
88
|
-
合并单位是「任务」不是文本行 —— 行级合并只认相邻上下文:不同任务各自新增 /
|
|
89
|
-
结案挤在文件末尾就会撞,而同一任务两边都改时它也判不出该留谁。按 id 对账后,
|
|
90
|
-
只有一边动过的直接采纳,两边都动过的列进冲突,一个都不静默丢。
|
|
91
|
-
"""
|
|
92
|
-
b, o, t = (_coerce(x) for x in (base, ours, theirs))
|
|
93
|
-
if b is None or o is None or t is None:
|
|
94
|
-
return {}, ["台账冲突无法自动合并:有一方不是合法 JSON,需要人工处理。"]
|
|
95
|
-
merged: dict[str, Any] = dict(o)
|
|
96
|
-
for key, value in t.items():
|
|
97
|
-
if key not in o:
|
|
98
|
-
merged[key] = value
|
|
99
|
-
elif o[key] != value and key not in EXPAND_KEYS:
|
|
100
|
-
merged[key] = value # 非任务区(version / identity)以本机那份为准
|
|
101
|
-
conflicts: list[str] = []
|
|
102
|
-
for expand in EXPAND_KEYS:
|
|
103
|
-
if not any(expand in x for x in (b, o, t)):
|
|
104
|
-
continue # 这份文件没有这个区(pool.json 没有 workers、分片没有 tasks)
|
|
105
|
-
base_rows, our_rows, their_rows = (
|
|
106
|
-
_rows_of(b, expand),
|
|
107
|
-
_rows_of(o, expand),
|
|
108
|
-
_rows_of(t, expand),
|
|
109
|
-
)
|
|
110
|
-
rows: dict[str, Any] = {}
|
|
111
|
-
for tid in sorted(set(base_rows) | set(our_rows) | set(their_rows)):
|
|
112
|
-
was, up, loc = base_rows.get(tid), our_rows.get(tid), their_rows.get(tid)
|
|
113
|
-
if up == loc:
|
|
114
|
-
if up is not None:
|
|
115
|
-
rows[tid] = up
|
|
116
|
-
elif up == was:
|
|
117
|
-
if loc is not None:
|
|
118
|
-
rows[tid] = loc
|
|
119
|
-
elif loc == was:
|
|
120
|
-
# 只有上游动过:上游删了就真删掉。写成 rows[tid] = up 会在文件里
|
|
121
|
-
# 留一行 "<tid>": null 永久累积,读侧还会因 null 崩掉或整区降级。
|
|
122
|
-
if up is not None:
|
|
123
|
-
rows[tid] = up
|
|
124
|
-
else:
|
|
125
|
-
conflicts.append(tid)
|
|
126
|
-
rows[tid] = up if up is not None else loc
|
|
127
|
-
merged[expand] = rows
|
|
128
|
-
return merged, conflicts
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def union_merge(base: str, ours: str, theirs: str) -> str:
|
|
132
|
-
"""追加式正文的三路合并:两边各自新增的行都要在,不凭行号猜。
|
|
133
|
-
|
|
134
|
-
过程文档 / 历史只追加不改写,所以"合并"= 本机那份 + 上游相对共同祖先新增的行
|
|
135
|
-
(本机已有同样的行就不再重复)。任一侧为空也照常合。
|
|
136
|
-
"""
|
|
137
|
-
base_lines = set(base.splitlines())
|
|
138
|
-
have = set(theirs.splitlines())
|
|
139
|
-
added = [
|
|
140
|
-
line
|
|
141
|
-
for line in ours.splitlines()
|
|
142
|
-
if line not in base_lines and line not in have
|
|
143
|
-
]
|
|
144
|
-
if not added:
|
|
145
|
-
return theirs
|
|
146
|
-
head = theirs if theirs.endswith("\n") or not theirs else theirs + "\n"
|
|
147
|
-
return head + "\n".join(added) + "\n"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def conflict_marked(
|
|
151
|
-
rel: str, base: str, ours: str, theirs: str
|
|
152
|
-
) -> tuple[str, list[str]] | None:
|
|
153
|
-
"""接管一份 JSON 台账冲突:算出「一条一行 + 冲突标记」的新全文。
|
|
154
|
-
|
|
155
|
-
返回 (文件新文本, 两边都改过的任务号);不是台账 JSON 或有一方解析不了 ⇒ None。
|
|
156
|
-
"""
|
|
157
|
-
if not is_ledger_json(rel):
|
|
158
|
-
return None
|
|
159
|
-
data, conflicts = merge_task_dicts(base, ours, theirs)
|
|
160
|
-
if not data:
|
|
161
|
-
return None
|
|
162
|
-
key = expand_key_of(rel)
|
|
163
|
-
if not conflicts:
|
|
164
|
-
return dump_ledger_json(data, expand=key), []
|
|
165
|
-
return (
|
|
166
|
-
_marked_text(
|
|
167
|
-
_load(theirs) or {},
|
|
168
|
-
key,
|
|
169
|
-
data,
|
|
170
|
-
conflicts,
|
|
171
|
-
_load(ours) or {},
|
|
172
|
-
_load(theirs) or {},
|
|
173
|
-
),
|
|
174
|
-
conflicts,
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _marked_text(
|
|
179
|
-
full: dict[str, Any],
|
|
180
|
-
key: str,
|
|
181
|
-
merged: dict[str, Any],
|
|
182
|
-
conflicts: list[str],
|
|
183
|
-
upstream: dict[str, Any],
|
|
184
|
-
local: dict[str, Any],
|
|
185
|
-
) -> str:
|
|
186
|
-
"""按「一条一行」渲染整份台账;两边都改过的条目用冲突标记包起来。
|
|
187
|
-
|
|
188
|
-
`full` 是本机那一侧的整份结构(version / identity / tasks),除 `key` 之外
|
|
189
|
-
原样保留 —— 分片里的 identity 丢了就没法自证。
|
|
190
|
-
"""
|
|
191
|
-
rows: list[str] = []
|
|
192
|
-
for field in sorted(full):
|
|
193
|
-
if field != key:
|
|
194
|
-
rows.append(
|
|
195
|
-
f" {json.dumps(field, ensure_ascii=False)}: "
|
|
196
|
-
f"{json.dumps(full[field], ensure_ascii=False, sort_keys=True)}"
|
|
197
|
-
)
|
|
198
|
-
continue
|
|
199
|
-
inner: list[str] = []
|
|
200
|
-
merged_rows = merged.get(key) or {}
|
|
201
|
-
for ident in sorted(merged_rows):
|
|
202
|
-
label = json.dumps(ident, ensure_ascii=False)
|
|
203
|
-
if ident not in conflicts:
|
|
204
|
-
inner.append(
|
|
205
|
-
f" {label}: "
|
|
206
|
-
f"{json.dumps(merged_rows[ident], ensure_ascii=False, sort_keys=True)}"
|
|
207
|
-
)
|
|
208
|
-
continue
|
|
209
|
-
left = (
|
|
210
|
-
"(远端已删除)"
|
|
211
|
-
if _rows_of(upstream, key).get(ident) is None
|
|
212
|
-
else json.dumps(
|
|
213
|
-
_rows_of(upstream, key)[ident], ensure_ascii=False, sort_keys=True
|
|
214
|
-
)
|
|
215
|
-
)
|
|
216
|
-
right = (
|
|
217
|
-
"(本地已删除)"
|
|
218
|
-
if _rows_of(local, key).get(ident) is None
|
|
219
|
-
else json.dumps(
|
|
220
|
-
_rows_of(local, key)[ident], ensure_ascii=False, sort_keys=True
|
|
221
|
-
)
|
|
222
|
-
)
|
|
223
|
-
inner.append(
|
|
224
|
-
f"<<<<<<< 远端\n {label}: {left}\n=======\n"
|
|
225
|
-
f" {label}: {right}\n>>>>>>> 本地"
|
|
226
|
-
)
|
|
227
|
-
rows.append(
|
|
228
|
-
f' "{key}": {{\n' + (",\n".join(inner) + "\n" if inner else "") + " }"
|
|
229
|
-
)
|
|
230
|
-
return "{\n" + ",\n".join(rows) + "\n}\n"
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def resolve_ledger_conflict(path: Path, text: str) -> str | None:
|
|
234
|
-
"""把合并结果写回冲突文件(`track=False`、不进记账本),写完由 `git rebase --continue` 提交。
|
|
235
|
-
|
|
236
|
-
返回写回的文本;`text` 为空(说明没算出合并结果)返回 None。
|
|
237
|
-
"""
|
|
238
|
-
if not text:
|
|
239
|
-
return None
|
|
240
|
-
write_text_atomic(Path(path), text, track=False)
|
|
241
|
-
return text
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def render_conflict_text(rel: str, base: str, ours: str, theirs: str) -> str:
|
|
245
|
-
"""人话冲突说明:**返回文本,不打印**(打印归 render.py)。"""
|
|
246
|
-
if not owns(rel):
|
|
247
|
-
return f"{rel}:不是台账文件,脚本不代劳,请人工处理。"
|
|
248
|
-
if is_append_only(rel):
|
|
249
|
-
return f"{rel}:两边都追加过,已并成一份(追加式正文,一行没丢)。"
|
|
250
|
-
data, conflicts = merge_task_dicts(base, ours, theirs)
|
|
251
|
-
if not data:
|
|
252
|
-
return f"{rel}:有一方不是合法 JSON,无法自动合并,请人工处理。"
|
|
253
|
-
if not conflicts:
|
|
254
|
-
return f"{rel}:冲突已按任务号自动合并,无需人工处理。"
|
|
255
|
-
lines = [
|
|
256
|
-
(
|
|
257
|
-
f"{rel}:这些任务被两边同时改过,已按任务逐个标出冲突"
|
|
258
|
-
"(标记里左边是远端、右边是本机),没有替你选任何一个:"
|
|
259
|
-
),
|
|
260
|
-
]
|
|
261
|
-
lines.extend(f" - {tid}" for tid in conflicts)
|
|
262
|
-
lines.append(" 处理:编辑该文件保留你要的版本 → git add → git rebase --continue;")
|
|
263
|
-
lines.append(
|
|
264
|
-
" 不想合就 git rebase --abort 回到写之前。你的台账改动已提交在本地,不会丢。"
|
|
265
|
-
)
|
|
266
|
-
return "\n".join(lines)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""取最新要刷哪些文件:**全仓唯一算锁集的地方**。
|
|
2
|
-
|
|
3
|
-
返回两份 `Scope`(自举那份、动作那份)。runner 拿它做三件事:
|
|
4
|
-
`sync_down(boot | act)` / `atom(*boot.files)` 包自举 / `atom(*act.files)` 包动作。
|
|
5
|
-
它本来就不碰 git —— `--no-sync` 也照调。
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
from ..actions.atom import Scope, lock_scope
|
|
14
|
-
from ..core.identity import Identity
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def presync_scope(
|
|
18
|
-
repo: Path, me: Identity, action: str, params: dict[str, Any]
|
|
19
|
-
) -> tuple[Scope, Scope]:
|
|
20
|
-
"""(自举那份, 动作那份)。只读动作由 runner 直接拿空的两份,不进这里。"""
|
|
21
|
-
boot = lock_scope(repo, me, "bootstrap", params)
|
|
22
|
-
act = Scope() if action == "bootstrap" else lock_scope(repo, me, action, params)
|
|
23
|
-
return boot, act
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
"""唯一打印处。一个返回类型一个函数,**不做统一分发**;也**不 flush**。
|
|
2
|
-
|
|
3
|
-
`flush_notices()` 只在 `cli.main` 的 `finally` 里调一次 —— 提交 / 推送阶段冒出来的提示也才打得到。
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import sys
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
from .. import notices
|
|
13
|
-
from ..actions.query import HistoryQuery, ListQuery
|
|
14
|
-
from ..actions.result import (
|
|
15
|
-
HistoryResult,
|
|
16
|
-
ListResult,
|
|
17
|
-
Result,
|
|
18
|
-
ShowResult,
|
|
19
|
-
ValidationReport,
|
|
20
|
-
)
|
|
21
|
-
from ..core.identity import Identity
|
|
22
|
-
from ..core.time import fmt_local
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _out(text: str = "") -> None:
|
|
26
|
-
sys.stdout.write(text + "\n")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _err(text: str) -> None:
|
|
30
|
-
sys.stderr.write(text + "\n")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def flush_notices() -> None:
|
|
34
|
-
"""把 `notices` 一次性打到 stderr(退出前调一次,同一句只打一次)。"""
|
|
35
|
-
for line in notices.warnings():
|
|
36
|
-
_err(line)
|
|
37
|
-
notices.clear()
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def fail_message(exc: BaseException, *, title: str = "错误") -> None:
|
|
41
|
-
"""业务错误 / 未预期错误的 stderr 输出。"""
|
|
42
|
-
_err(f"{title}:{exc}")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def repo_banner(repo: Any, *, machine: bool) -> None:
|
|
46
|
-
"""只读命令开头自报「读的是哪个台账」(输出是 JSON 时不打,否则污染机器可读结果)。"""
|
|
47
|
-
if not machine:
|
|
48
|
-
_out(f"台账:{repo}")
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _json(data: Any) -> None:
|
|
52
|
-
sys.stdout.write(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
|
|
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
|
-
|
|
72
|
-
def result(res: Result, *, as_json: bool) -> None:
|
|
73
|
-
"""写动作:一律人话(`--json` 不挂写动作,传进来也按人话打)。"""
|
|
74
|
-
del as_json
|
|
75
|
-
if res.already_done:
|
|
76
|
-
_out("无需重复:这次没有新的改动。")
|
|
77
|
-
else:
|
|
78
|
-
_out(f"已改:{'、'.join(res.changed) if res.changed else '(无字段改动)'}")
|
|
79
|
-
if res.where:
|
|
80
|
-
_out(f"落点:{res.where}")
|
|
81
|
-
for line in res.notices:
|
|
82
|
-
_err(line)
|
|
83
|
-
if res.worklog is not None:
|
|
84
|
-
if res.worklog.path:
|
|
85
|
-
_out(f"过程文档:{res.worklog.path}")
|
|
86
|
-
elif res.worklog.error:
|
|
87
|
-
_err(f"过程文档:{res.worklog.error}")
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def list_view(res: ListResult, q: ListQuery | None, *, as_json: bool) -> None:
|
|
91
|
-
if as_json:
|
|
92
|
-
_json({tid: localize_times(task) for tid, task in res.rows.items()})
|
|
93
|
-
return
|
|
94
|
-
if not res.table:
|
|
95
|
-
_out("没有符合条件的在办任务。")
|
|
96
|
-
return
|
|
97
|
-
_out(f"在办 {len(res.table)} 条:")
|
|
98
|
-
for row in res.table:
|
|
99
|
-
owner = row.owner or "(未认领)"
|
|
100
|
-
_out(f" {row.tid} {row.status:<6} {owner:<12} {row.title}")
|
|
101
|
-
for line in res.notices:
|
|
102
|
-
_err(line)
|
|
103
|
-
|
|
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
|
-
|
|
125
|
-
def show_view(res: ShowResult, *, as_json: bool) -> None:
|
|
126
|
-
if as_json:
|
|
127
|
-
_json(
|
|
128
|
-
{
|
|
129
|
-
"task": localize_times(res.task),
|
|
130
|
-
# 文档自述还是回退查在办:调用方据此知道字段来源(老文档 / 文档缺失)
|
|
131
|
-
"legacy": res.legacy,
|
|
132
|
-
"worklog": {
|
|
133
|
-
"path": res.worklog.path if res.worklog else None,
|
|
134
|
-
"text": res.worklog.text if res.worklog else None,
|
|
135
|
-
"error": res.worklog.error if res.worklog else None,
|
|
136
|
-
},
|
|
137
|
-
"others": list(res.others),
|
|
138
|
-
}
|
|
139
|
-
)
|
|
140
|
-
return
|
|
141
|
-
task = res.task
|
|
142
|
-
_out(f"{task.get('tid', '')}:{task.get('title', '')}")
|
|
143
|
-
_out(
|
|
144
|
-
f" 状态:{task.get('status', '')} "
|
|
145
|
-
f"负责人:{task.get('owner_name') or task.get('owner') or '(未认领)'}"
|
|
146
|
-
)
|
|
147
|
-
_out(
|
|
148
|
-
f" 创建:{fmt_local(task.get('created'))} "
|
|
149
|
-
f"最后更新:{fmt_local(task.get('updated'))}"
|
|
150
|
-
)
|
|
151
|
-
if res.others:
|
|
152
|
-
_out(f" 还出现在:{'、'.join(res.others)}")
|
|
153
|
-
if res.worklog is None:
|
|
154
|
-
return
|
|
155
|
-
if res.worklog.error:
|
|
156
|
-
_err(f" 过程文档:{res.worklog.error}")
|
|
157
|
-
elif res.worklog.path:
|
|
158
|
-
_out(f" 过程文档:{res.worklog.path}")
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def history_view(res: HistoryResult, q: HistoryQuery | None, *, as_json: bool) -> None:
|
|
162
|
-
# `--count-only` 是**计数口径**:固定两个键的 JSON(§六 / ledger-data.md 第七节)。
|
|
163
|
-
# 它与 `--json` 互斥(同给即用法错),所以这里不看 as_json —— 否则那个分支永远是死代码。
|
|
164
|
-
if q is not None and q.count_only:
|
|
165
|
-
_json({"count": res.count, "capped": res.capped})
|
|
166
|
-
return
|
|
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])
|
|
172
|
-
_json(
|
|
173
|
-
{
|
|
174
|
-
"count": res.count,
|
|
175
|
-
"filters": filters,
|
|
176
|
-
"records": [localize_times(row) for row in res.records],
|
|
177
|
-
"warnings": list(res.warnings),
|
|
178
|
-
"capped": res.capped,
|
|
179
|
-
}
|
|
180
|
-
)
|
|
181
|
-
return
|
|
182
|
-
for line in res.warnings:
|
|
183
|
-
_err(f"警告:{line}")
|
|
184
|
-
if not res.records:
|
|
185
|
-
_out("没有符合条件的结案记录。")
|
|
186
|
-
return
|
|
187
|
-
_out(f"已结案 {res.count} 条:")
|
|
188
|
-
for row in res.records:
|
|
189
|
-
_out(
|
|
190
|
-
f" {fmt_local(row.get('finished'))} "
|
|
191
|
-
f"{row.get('owner_name') or row.get('owner') or '(无人)':<12} {row.get('title', '')}"
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def validate_view(rep: ValidationReport, *, as_json: bool) -> None:
|
|
196
|
-
if as_json:
|
|
197
|
-
_json(
|
|
198
|
-
{
|
|
199
|
-
"problems": list(rep.problems),
|
|
200
|
-
"notices": list(rep.notices),
|
|
201
|
-
"checked_files": rep.checked_files,
|
|
202
|
-
"total_tasks": rep.total_tasks,
|
|
203
|
-
"deep": rep.deep,
|
|
204
|
-
}
|
|
205
|
-
)
|
|
206
|
-
return
|
|
207
|
-
mode = "深度" if rep.deep else "浅度"
|
|
208
|
-
_out(f"体检({mode}):查了 {rep.checked_files} 个文件、{rep.total_tasks} 条任务。")
|
|
209
|
-
if rep.problems:
|
|
210
|
-
_out(f"问题 {len(rep.problems)} 条:")
|
|
211
|
-
for line in rep.problems:
|
|
212
|
-
_out(f" - {line}")
|
|
213
|
-
else:
|
|
214
|
-
_out("没有问题。")
|
|
215
|
-
for line in rep.notices:
|
|
216
|
-
_err(f"提示:{line}")
|