dsh-task-ledger 0.0.100 → 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.
Files changed (68) hide show
  1. package/dist/client.js +2 -2
  2. package/dist/host/repositories/runner.d.ts +1 -1
  3. package/dist/host/repositories/runner.js +10 -7
  4. package/dist/host/skill/registration.d.ts +2 -1
  5. package/dist/host/skill/registration.js +4 -3
  6. package/package.json +1 -1
  7. package/skill/README.md +4 -2
  8. package/skill/SKILL.md +17 -14
  9. package/skill/assets/worklog.md +3 -3
  10. package/skill/package.json +1 -1
  11. package/skill/references/init-identity.md +3 -3
  12. package/skill/references/log-note.md +1 -1
  13. package/skill/references/state-model.md +1 -1
  14. package/skill/references/troubleshooting.md +1 -1
  15. package/skill/scripts/bin/ledger-darwin-arm64 +0 -0
  16. package/skill/scripts/bin/ledger-linux-amd64 +0 -0
  17. package/skill/scripts/bin/ledger-windows-amd64.exe +0 -0
  18. package/skill/scripts/progress +44 -0
  19. package/skill/scripts/ledgerkit/__init__.py +0 -0
  20. package/skill/scripts/ledgerkit/actions/__init__.py +0 -0
  21. package/skill/scripts/ledgerkit/actions/atom.py +0 -154
  22. package/skill/scripts/ledgerkit/actions/bootstrap.py +0 -37
  23. package/skill/scripts/ledgerkit/actions/claim.py +0 -88
  24. package/skill/scripts/ledgerkit/actions/close_task.py +0 -193
  25. package/skill/scripts/ledgerkit/actions/guard.py +0 -158
  26. package/skill/scripts/ledgerkit/actions/handoff.py +0 -93
  27. package/skill/scripts/ledgerkit/actions/init_identity.py +0 -90
  28. package/skill/scripts/ledgerkit/actions/list_tasks.py +0 -74
  29. package/skill/scripts/ledgerkit/actions/log_progress.py +0 -67
  30. package/skill/scripts/ledgerkit/actions/new_task.py +0 -130
  31. package/skill/scripts/ledgerkit/actions/query.py +0 -35
  32. package/skill/scripts/ledgerkit/actions/query_history.py +0 -220
  33. package/skill/scripts/ledgerkit/actions/registry.py +0 -82
  34. package/skill/scripts/ledgerkit/actions/relabel.py +0 -105
  35. package/skill/scripts/ledgerkit/actions/release.py +0 -76
  36. package/skill/scripts/ledgerkit/actions/result.py +0 -90
  37. package/skill/scripts/ledgerkit/actions/set_task.py +0 -73
  38. package/skill/scripts/ledgerkit/actions/show_task.py +0 -55
  39. package/skill/scripts/ledgerkit/actions/validate.py +0 -189
  40. package/skill/scripts/ledgerkit/core/__init__.py +0 -0
  41. package/skill/scripts/ledgerkit/core/history_md.py +0 -152
  42. package/skill/scripts/ledgerkit/core/identity.py +0 -54
  43. package/skill/scripts/ledgerkit/core/ids.py +0 -58
  44. package/skill/scripts/ledgerkit/core/labels.py +0 -90
  45. package/skill/scripts/ledgerkit/core/paths.py +0 -92
  46. package/skill/scripts/ledgerkit/core/roots.py +0 -74
  47. package/skill/scripts/ledgerkit/core/time.py +0 -103
  48. package/skill/scripts/ledgerkit/core/worklog_md.py +0 -437
  49. package/skill/scripts/ledgerkit/env.py +0 -94
  50. package/skill/scripts/ledgerkit/errors.py +0 -15
  51. package/skill/scripts/ledgerkit/notices.py +0 -28
  52. package/skill/scripts/ledgerkit/shell/__init__.py +0 -0
  53. package/skill/scripts/ledgerkit/shell/cli.py +0 -536
  54. package/skill/scripts/ledgerkit/shell/gitops.py +0 -668
  55. package/skill/scripts/ledgerkit/shell/merge.py +0 -266
  56. package/skill/scripts/ledgerkit/shell/presync.py +0 -23
  57. package/skill/scripts/ledgerkit/shell/render.py +0 -216
  58. package/skill/scripts/ledgerkit/shell/runner.py +0 -232
  59. package/skill/scripts/ledgerkit/store/__init__.py +0 -0
  60. package/skill/scripts/ledgerkit/store/atomic.py +0 -130
  61. package/skill/scripts/ledgerkit/store/dirty.py +0 -108
  62. package/skill/scripts/ledgerkit/store/history_store.py +0 -134
  63. package/skill/scripts/ledgerkit/store/json_store.py +0 -166
  64. package/skill/scripts/ledgerkit/store/locks.py +0 -64
  65. package/skill/scripts/ledgerkit/store/owners_index.py +0 -127
  66. package/skill/scripts/ledgerkit/store/task_store.py +0 -28
  67. package/skill/scripts/ledgerkit/store/worklog_store.py +0 -180
  68. 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}")