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.
- 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 +17 -14
- package/skill/assets/worklog.md +3 -3
- 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/state-model.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 -193
- 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 -130
- 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 -105
- 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 -73
- 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 -90
- 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 -437
- 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 -536
- 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 -180
- package/skill/scripts/progress.py +0 -20
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
"""改分类 / 标签:分片那两个字段 + 过程文档头部那行,一次改动两处同步。
|
|
2
|
-
|
|
3
|
-
只动「分类 / 标签」这一件事,不碰归属与状态 —— 归属走 `--owner` / `--release`,
|
|
4
|
-
状态只随命令语义变。已结案的任务不在任何分片里,改不了(历史行是快照)。
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from ..core.identity import Identity
|
|
12
|
-
from ..core.labels import normalize_label, parse_tags
|
|
13
|
-
from ..core.paths import shard_rel
|
|
14
|
-
from ..core.time import now
|
|
15
|
-
from ..core.worklog_md import (
|
|
16
|
-
StatusLine,
|
|
17
|
-
append_exec_log,
|
|
18
|
-
exec_line,
|
|
19
|
-
rewrite_label_line,
|
|
20
|
-
rewrite_status_line,
|
|
21
|
-
)
|
|
22
|
-
from ..errors import Fail
|
|
23
|
-
from ..store.task_store import TaskStore
|
|
24
|
-
from ..store.worklog_store import WorklogStore
|
|
25
|
-
from .guard import require_owner
|
|
26
|
-
from .result import Result, rel
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _describe(category: str | None, tags: list[str]) -> str:
|
|
30
|
-
"""执行记录里那一句说明:`建设 · DSH、台账`。"""
|
|
31
|
-
parts = [category] if category else []
|
|
32
|
-
if tags:
|
|
33
|
-
parts.append("、".join(tags))
|
|
34
|
-
return " · ".join(parts)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def relabel(
|
|
38
|
-
repo: Path,
|
|
39
|
-
me: Identity,
|
|
40
|
-
tid: str,
|
|
41
|
-
*,
|
|
42
|
-
category: str | None = None,
|
|
43
|
-
tags: str | tuple[str, ...] | None = None,
|
|
44
|
-
) -> Result:
|
|
45
|
-
"""写序:算最终值 → 过程文档(标签行 + 执行记录 + 状态行,一次写)→ 分片。
|
|
46
|
-
|
|
47
|
-
`tags` 收字符串(顿号 / 逗号分隔)或已切好的元组 —— 外壳可能已经切过一遍。
|
|
48
|
-
"""
|
|
49
|
-
new_category = normalize_label(category, field="分类") if category else None
|
|
50
|
-
new_tags = parse_tags(tags) if tags else None
|
|
51
|
-
if new_category is None and new_tags is None:
|
|
52
|
-
raise Fail(
|
|
53
|
-
"改分类 / 标签至少要给一个:`--category <分类>` 或 `--tags <a、b>`。"
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
mine = TaskStore(repo, shard_rel(me.uuid), identity=me)
|
|
57
|
-
task = mine.get(tid) # 锁内重读
|
|
58
|
-
if task is None:
|
|
59
|
-
raise Fail(
|
|
60
|
-
f"任务 {tid} 不在我的分片里(只能改自己名下在办的任务;已结案的任务改不了)。"
|
|
61
|
-
)
|
|
62
|
-
require_owner(task, tid, me)
|
|
63
|
-
|
|
64
|
-
# 没给的沿用原值:分类与标签是一行里的两段,写一次就得给全
|
|
65
|
-
final_category = new_category if new_category else task.get("category")
|
|
66
|
-
final_tags: list[str] = (
|
|
67
|
-
[str(tag) for tag in new_tags]
|
|
68
|
-
if new_tags is not None
|
|
69
|
-
else [str(tag) for tag in (task.get("tags") or ())]
|
|
70
|
-
)
|
|
71
|
-
if not final_category and not final_tags:
|
|
72
|
-
raise Fail("分类与标签都是空的,没有可写的值。")
|
|
73
|
-
|
|
74
|
-
when = now()
|
|
75
|
-
updated = dict(task)
|
|
76
|
-
updated["category"] = final_category
|
|
77
|
-
updated["tags"] = final_tags
|
|
78
|
-
updated["updated"] = when
|
|
79
|
-
|
|
80
|
-
logs = WorklogStore(repo)
|
|
81
|
-
path = logs.find(tid, task.get("worklog"))
|
|
82
|
-
if path is None:
|
|
83
|
-
mine.put(tid, updated)
|
|
84
|
-
return Result(
|
|
85
|
-
changed=("category", "tags", "updated"),
|
|
86
|
-
where=rel(repo, mine.path),
|
|
87
|
-
message=f"{tid}:改分类 / 标签",
|
|
88
|
-
notices=("提示:这条任务没有过程文档,只改了分片,文档与历史里看不到。",),
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
text = logs.read(path)
|
|
92
|
-
note = "改分类 / 标签:" + _describe(final_category, final_tags)
|
|
93
|
-
text = rewrite_label_line(text, final_category, final_tags)
|
|
94
|
-
text = append_exec_log(text, exec_line(when, me.stamp(), "set", note))
|
|
95
|
-
text = rewrite_status_line(
|
|
96
|
-
text, StatusLine(task.get("status") or "doing", me.name, me.uuid, when)
|
|
97
|
-
)
|
|
98
|
-
logs.write(path, text)
|
|
99
|
-
mine.put(tid, updated)
|
|
100
|
-
|
|
101
|
-
return Result(
|
|
102
|
-
changed=("category", "tags", "updated"),
|
|
103
|
-
where=rel(repo, mine.path),
|
|
104
|
-
message=f"{tid}:改分类 / 标签",
|
|
105
|
-
)
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""释放:写回公共池,再删我分片那行,再记一笔。"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from ..core.identity import Identity
|
|
8
|
-
from ..core.paths import POOL_REL, shard_rel
|
|
9
|
-
from ..core.time import now
|
|
10
|
-
from ..core.worklog_md import StatusLine, exec_line
|
|
11
|
-
from ..errors import Fail
|
|
12
|
-
from ..store.task_store import TaskStore
|
|
13
|
-
from ..store.worklog_store import WorklogStore
|
|
14
|
-
from .guard import owner_of, require_owner, status_of
|
|
15
|
-
from .result import Result, rel
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def release(repo: Path, me: Identity, tid: str) -> Result:
|
|
19
|
-
"""写序:写回池(已有同号就不覆盖)→ 删我分片那行 → 过程文档记一笔。"""
|
|
20
|
-
pool = TaskStore(repo, POOL_REL)
|
|
21
|
-
mine = TaskStore(repo, shard_rel(me.uuid), identity=me)
|
|
22
|
-
task = mine.get(tid) # 锁内重读
|
|
23
|
-
if task is None:
|
|
24
|
-
in_pool = pool.get(tid)
|
|
25
|
-
if in_pool is not None:
|
|
26
|
-
# 幂等分支也要验「池里那条真的已经是没人要的开放任务」,
|
|
27
|
-
# 否则池里混进带 owner 的脏行时,release 会报「无需重复释放」而任务实际仍被占
|
|
28
|
-
if owner_of(in_pool) or status_of(in_pool) != "open":
|
|
29
|
-
raise Fail(
|
|
30
|
-
f"任务 {tid} 在公共池里,但身上还挂着 owner / 不是 open —— 台账脏了。\n"
|
|
31
|
-
" 处理:跑 validate 看清单,别用 release 强行覆盖。"
|
|
32
|
-
)
|
|
33
|
-
return Result(
|
|
34
|
-
already_done=True,
|
|
35
|
-
where=rel(repo, pool.path),
|
|
36
|
-
notices=(f"任务 {tid} 已经在公共池里,无需重复释放。",),
|
|
37
|
-
)
|
|
38
|
-
raise Fail(f"任务 {tid} 不在我的分片里,也没在公共池里。")
|
|
39
|
-
|
|
40
|
-
# 分片里在、但行内 owner 不是我 ⇒ 台账脏了(validate ⑤):不许把别人的行推进池
|
|
41
|
-
require_owner(task, tid, me)
|
|
42
|
-
|
|
43
|
-
back = dict(task)
|
|
44
|
-
back["owner"] = None
|
|
45
|
-
back["owner_name"] = None # 冗余显示名跟着 owner 一起清,否则池里那条仍显示原负责人
|
|
46
|
-
back["status"] = "open"
|
|
47
|
-
back["updated"] = now()
|
|
48
|
-
if pool.get(tid) is None:
|
|
49
|
-
pool.put(tid, back)
|
|
50
|
-
else:
|
|
51
|
-
# 池里已有同号:那份必须是干净的空位,否则不写回(它的 owner/owner_name 会被旧值带脏)
|
|
52
|
-
existing = pool.get(tid) or {}
|
|
53
|
-
if owner_of(existing) or status_of(existing) != "open":
|
|
54
|
-
raise Fail(
|
|
55
|
-
f"公共池里已经有一条 {tid},且它带着 owner / 不是 open —— 台账脏了。\n"
|
|
56
|
-
" 处理:跑 validate 看清单,先确认哪条是真的。"
|
|
57
|
-
)
|
|
58
|
-
mine.drop(tid)
|
|
59
|
-
|
|
60
|
-
logs = WorklogStore(repo)
|
|
61
|
-
path = logs.find(tid, back.get("worklog"))
|
|
62
|
-
note = None
|
|
63
|
-
if path is None:
|
|
64
|
-
note = f"任务 {tid} 已退回公共池,但过程文档没找到,跳过执行记录。"
|
|
65
|
-
else:
|
|
66
|
-
logs.append_exec(
|
|
67
|
-
path,
|
|
68
|
-
exec_line(now(), me.stamp(), "release", "释放回公共池"),
|
|
69
|
-
status=StatusLine("open", None, None, now()),
|
|
70
|
-
)
|
|
71
|
-
return Result(
|
|
72
|
-
changed=("owner", "owner_name", "status", "updated"),
|
|
73
|
-
where=rel(repo, pool.path),
|
|
74
|
-
message=f"释放 {tid}:{back.get('title') or ''}".strip(),
|
|
75
|
-
notices=(note,) if note else (),
|
|
76
|
-
)
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
"""动作的返回值长什么样。字段名是**公开契约**,插件看板直接读,外壳不许重新算。
|
|
2
|
-
|
|
3
|
-
写动作返回 `Result`,只读动作返回各自的视图对象(不再套一层统一的壳)。
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from dataclasses import dataclass, field
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def rel(repo: Path, path: Path) -> str:
|
|
14
|
-
"""绝对路径 → 相对 repo 的字符串(输出给人 / 给界面的一律这个基准)。"""
|
|
15
|
-
try:
|
|
16
|
-
return str(Path(path).resolve().relative_to(Path(repo).resolve()))
|
|
17
|
-
except ValueError:
|
|
18
|
-
return str(path)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclass(frozen=True)
|
|
22
|
-
class WorklogView:
|
|
23
|
-
"""「这条任务的过程文档要怎么显示」。"""
|
|
24
|
-
|
|
25
|
-
path: str | None = None # 相对 repo
|
|
26
|
-
text: str | None = None # 全文;读不到时为 None
|
|
27
|
-
error: str | None = None # 读不到的原因(缺失 / 读不了),给人看
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@dataclass(frozen=True)
|
|
31
|
-
class Result:
|
|
32
|
-
"""写动作的返回。"""
|
|
33
|
-
|
|
34
|
-
changed: tuple[str, ...] = () # 改了哪些**字段名**(不是文件名、不是任务号)
|
|
35
|
-
where: str | None = None # 主要落在哪个文件(**相对 repo**)
|
|
36
|
-
already_done: bool = False # 重跑发现已经做过 ⇒ 退出码仍是 0
|
|
37
|
-
message: str | None = None # git 提交说明;None ⇒ 外壳用默认模板
|
|
38
|
-
worklog: WorklogView | None = None
|
|
39
|
-
payload: dict[str, Any] = field(default_factory=dict)
|
|
40
|
-
notices: tuple[str, ...] = ()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@dataclass(frozen=True)
|
|
44
|
-
class TaskRow:
|
|
45
|
-
"""人话表格的一行(9 字段投影)。`--json` 吐的是 `rows`(原样任务对象),不是它。"""
|
|
46
|
-
|
|
47
|
-
tid: str
|
|
48
|
-
title: str
|
|
49
|
-
status: str
|
|
50
|
-
owner: str | None = None # 显示名
|
|
51
|
-
created: str | None = None
|
|
52
|
-
started: str | None = None
|
|
53
|
-
category: str | None = None
|
|
54
|
-
tags: tuple[str, ...] = ()
|
|
55
|
-
where: str | None = None # 所在文件(相对 repo)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@dataclass(frozen=True)
|
|
59
|
-
class ListResult:
|
|
60
|
-
rows: dict[str, dict[str, Any]] = field(default_factory=dict) # tid → 原样任务对象
|
|
61
|
-
table: tuple[TaskRow, ...] = ()
|
|
62
|
-
notices: tuple[str, ...] = ()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@dataclass(frozen=True)
|
|
66
|
-
class ShowResult:
|
|
67
|
-
task: dict[str, Any] = field(default_factory=dict)
|
|
68
|
-
worklog: WorklogView | None = None
|
|
69
|
-
others: tuple[str, ...] = () # 同一个任务号还出现在哪些文件里
|
|
70
|
-
#: 字段是从**在办索引**读来的(老文档没有状态行 / 文档丢了),不是文档自述
|
|
71
|
-
legacy: bool = False
|
|
72
|
-
notices: tuple[str, ...] = ()
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
@dataclass(frozen=True)
|
|
76
|
-
class HistoryResult:
|
|
77
|
-
count: int = 0
|
|
78
|
-
filters: dict[str, Any] = field(default_factory=dict) # 回显生效的筛选
|
|
79
|
-
records: tuple[dict[str, Any], ...] = ()
|
|
80
|
-
warnings: tuple[str, ...] = ()
|
|
81
|
-
capped: bool = False
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@dataclass(frozen=True)
|
|
85
|
-
class ValidationReport:
|
|
86
|
-
problems: tuple[str, ...] = () # 只有它影响退出码
|
|
87
|
-
notices: tuple[str, ...] = () # 只提示,不改退出码
|
|
88
|
-
checked_files: int = 0
|
|
89
|
-
total_tasks: int = 0
|
|
90
|
-
deep: bool = False # 本次是深度检查(--deep)还是默认浅度
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
"""改归属的入口:只看 `--owner`,决定走哪个动作,**自己不加锁、不改任何文件**。
|
|
2
|
-
|
|
3
|
-
它是全树唯一的跨动作引用点(import `release` / `handoff` / `claim`)。
|
|
4
|
-
|
|
5
|
-
任务状态只随命令语义变(认领 → 进行中、释放 → 待认领、结案 → 历史),
|
|
6
|
-
**没有单独的「改状态」入口**:`--status` 于 2026-10-23 删除 —— 全仓没有任何
|
|
7
|
-
产品调用方(看板只有 claim / release / close 三个写路由),且它能制造
|
|
8
|
-
「归我名下、状态却还是待认领」的坏状态(看板上显示成没人认领)。
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
from ..core.identity import Identity, is_me
|
|
16
|
-
from ..core.paths import shard_rel
|
|
17
|
-
from ..errors import Fail
|
|
18
|
-
from ..store.task_store import TaskStore
|
|
19
|
-
from . import claim as claim_mod
|
|
20
|
-
from . import handoff as handoff_mod
|
|
21
|
-
from . import relabel as relabel_mod
|
|
22
|
-
from . import release as release_mod
|
|
23
|
-
from .guard import require_uuid
|
|
24
|
-
from .result import Result, rel
|
|
25
|
-
|
|
26
|
-
_MIXED = (
|
|
27
|
-
"--category / --tags 不能与 --release / --owner 同时给:"
|
|
28
|
-
"一个是改分类标签,一个是改归属,分两次跑。"
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def set_task(
|
|
33
|
-
repo: Path,
|
|
34
|
-
me: Identity,
|
|
35
|
-
tid: str,
|
|
36
|
-
*,
|
|
37
|
-
owner: str | None = None,
|
|
38
|
-
release: bool = False,
|
|
39
|
-
category: str | None = None,
|
|
40
|
-
tags: str | tuple[str, ...] | None = None,
|
|
41
|
-
) -> Result:
|
|
42
|
-
"""路由:`--release` = 释放;别人的 uuid = 交接;自己的 uuid = 已认领则空转、没认领先认领;
|
|
43
|
-
`--category` / `--tags` = 改分类与标签(只动文档头部那行与分片字段)。
|
|
44
|
-
"""
|
|
45
|
-
relabel_wanted = bool(category or tags)
|
|
46
|
-
if release:
|
|
47
|
-
if owner is not None:
|
|
48
|
-
raise Fail("--release 与 --owner 不能同时给:释放不带人,交接不带释放。")
|
|
49
|
-
if relabel_wanted:
|
|
50
|
-
raise Fail(_MIXED)
|
|
51
|
-
return release_mod.release(repo, me, tid)
|
|
52
|
-
if owner is not None:
|
|
53
|
-
if relabel_wanted:
|
|
54
|
-
raise Fail(_MIXED)
|
|
55
|
-
else:
|
|
56
|
-
if relabel_wanted:
|
|
57
|
-
return relabel_mod.relabel(repo, me, tid, category=category, tags=tags)
|
|
58
|
-
raise Fail(
|
|
59
|
-
"set 必须给一样:`--release`(释放回公共池)/ `--owner <对方 uuid>`(交接)/ "
|
|
60
|
-
"`--category <分类>` 或 `--tags <a、b>`(改分类标签)。"
|
|
61
|
-
)
|
|
62
|
-
# 交接目标只收 uuid:名字查不到就当场报错,绝不去猜「他大概是这个人」
|
|
63
|
-
target = require_uuid(owner, field="--owner")
|
|
64
|
-
if not is_me(me, target):
|
|
65
|
-
return handoff_mod.handoff(repo, me, tid, to=target)
|
|
66
|
-
mine = TaskStore(repo, shard_rel(me.uuid), identity=me)
|
|
67
|
-
if mine.get(tid) is not None:
|
|
68
|
-
return Result(
|
|
69
|
-
already_done=True,
|
|
70
|
-
where=rel(repo, mine.path),
|
|
71
|
-
notices=(f"任务 {tid} 已经在你名下,无需操作。",),
|
|
72
|
-
)
|
|
73
|
-
return claim_mod.claim(repo, me, tid)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""只读:看一条任务。**文档优先**——读到文档就知道状态,不再翻索引(2026-10-27)。
|
|
2
|
-
|
|
3
|
-
两条路径:
|
|
4
|
-
- 文档存在且头部有状态行 ⇒ 完全按文档自述回答(`status` / 负责人 / 更新时间 / 标题),
|
|
5
|
-
pool、分片、历史一个都不读,已结案的任务同样看得到;
|
|
6
|
-
- 文档没有状态行(老文档)或根本没有文档 ⇒ 回退 `load_task` 查在办索引,
|
|
7
|
-
返回里标 `legacy=True`,让调用方知道这些字段不是文档自述的。
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
from ..core import worklog_md
|
|
15
|
-
from ..core.identity import Identity
|
|
16
|
-
from ..store.worklog_store import WorklogStore
|
|
17
|
-
from .guard import load_task
|
|
18
|
-
from .result import ShowResult, WorklogView, rel
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def show_task(repo: Path, me: Identity, tid: str) -> ShowResult:
|
|
22
|
-
logs = WorklogStore(repo)
|
|
23
|
-
path = logs.find(tid) # 只按任务号找文件名:不碰 pool / 分片 / 历史
|
|
24
|
-
text = logs.read(path) if path is not None else ""
|
|
25
|
-
status = worklog_md.read_status(text) if text else None
|
|
26
|
-
if status is not None and path is not None:
|
|
27
|
-
head_tid, title = worklog_md.head_title(text)
|
|
28
|
-
return ShowResult(
|
|
29
|
-
task={
|
|
30
|
-
"tid": head_tid or tid,
|
|
31
|
-
"title": title,
|
|
32
|
-
"status": status.status,
|
|
33
|
-
"owner": status.uuid,
|
|
34
|
-
"owner_name": status.name,
|
|
35
|
-
"created": worklog_md.head_created(text),
|
|
36
|
-
"updated": status.when,
|
|
37
|
-
"finished": worklog_md.head_frozen(text),
|
|
38
|
-
},
|
|
39
|
-
worklog=WorklogView(path=rel(repo, path), text=text, error=None),
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# 老文档 / 文档缺失:回退查在办(找不到会抛,文案与既往一致)
|
|
43
|
-
task, where = load_task(repo, tid)
|
|
44
|
-
if path is None:
|
|
45
|
-
view = WorklogView(
|
|
46
|
-
path=None, text=None, error="这条任务没有过程文档(或文档已不在原处)。"
|
|
47
|
-
)
|
|
48
|
-
else:
|
|
49
|
-
view = WorklogView(path=rel(repo, path), text=text or None, error=None)
|
|
50
|
-
return ShowResult(
|
|
51
|
-
task=task,
|
|
52
|
-
worklog=view,
|
|
53
|
-
others=tuple(rel(repo, p) for p in where),
|
|
54
|
-
legacy=True,
|
|
55
|
-
)
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
"""只读:台账体检。两档:默认浅度只查格式,`--deep` 才读全部历史。
|
|
2
|
-
|
|
3
|
-
浅度(默认):池 + 各分片的字段格式、索引与文件对不对得上、**当月**历史。
|
|
4
|
-
深度(`--deep`):历史不止当月(全部月份)。
|
|
5
|
-
|
|
6
|
-
**两档都不读过程文档正文**(2026-10-27 用户拍板):过程文档是附件、存量可以上万篇,
|
|
7
|
-
逐篇读全文换不来体检结论,只白花时间;文档自身的格式与内容不在体检范围。
|
|
8
|
-
|
|
9
|
-
只查格式,不查业务(该谁结案、逾期与否都不管);也不查「同一任务号落在多个文件」——
|
|
10
|
-
读侧只显示一次(`list` / `show` 都不重复)、写侧已有防护,撞上了不关体检的事(2026-10-24 用户拍板)。
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
from ..core import history_md
|
|
18
|
-
from ..core.identity import Identity
|
|
19
|
-
from ..core.labels import STATUSES
|
|
20
|
-
from ..core.paths import (
|
|
21
|
-
LEDGER_DIR,
|
|
22
|
-
OWNERS_REL,
|
|
23
|
-
POOL_REL,
|
|
24
|
-
to_repo_rel,
|
|
25
|
-
)
|
|
26
|
-
from ..core.time import now
|
|
27
|
-
from ..store.history_store import HistoryStore
|
|
28
|
-
from ..store.owners_index import OwnersIndex
|
|
29
|
-
from ..store.task_store import TaskStore
|
|
30
|
-
from .guard import owner_of, status_of
|
|
31
|
-
from .result import ValidationReport, rel
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _read_tasks(repo: Path, store: TaskStore, problems: list[str]) -> dict:
|
|
35
|
-
"""读一个任务文件;撞上**程序自留**的冲突标记 ⇒ 记一条 problem 继续。
|
|
36
|
-
|
|
37
|
-
推送被拒后 rebase 给 JSON 留的 `<<<<<<<` 是程序自己产的坏数据,validate
|
|
38
|
-
要能指出来(退 1 给清单),而不是整条命令退 3(2026-10-24 修)。
|
|
39
|
-
其余读错误(用户手改坏)不在此列,照旧抛出。
|
|
40
|
-
"""
|
|
41
|
-
try:
|
|
42
|
-
return store.all()
|
|
43
|
-
except ValueError:
|
|
44
|
-
text = store.path.read_text(encoding="utf-8", errors="replace")
|
|
45
|
-
if "<<<<<<<" in text:
|
|
46
|
-
problems.append(
|
|
47
|
-
f"{rel(repo, store.path)} 带着冲突标记"
|
|
48
|
-
"(推送被拒后 rebase 没解完):先解决冲突,再跑 validate。"
|
|
49
|
-
)
|
|
50
|
-
return {}
|
|
51
|
-
raise
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _check_task_files(
|
|
55
|
-
repo: Path, index: OwnersIndex, problems: list[str]
|
|
56
|
-
) -> tuple[int, int]:
|
|
57
|
-
"""池 + 各分片的字段格式(owner / status / 分片归属);返回 (查了几个文件, 几条任务)。"""
|
|
58
|
-
checked = 0
|
|
59
|
-
total = 0
|
|
60
|
-
|
|
61
|
-
pool = TaskStore(repo, POOL_REL)
|
|
62
|
-
if pool.path.exists():
|
|
63
|
-
checked += 1
|
|
64
|
-
for tid, task in _read_tasks(repo, pool, problems).items():
|
|
65
|
-
total += 1
|
|
66
|
-
if owner_of(task):
|
|
67
|
-
problems.append(
|
|
68
|
-
f"公共池里的 {tid} 带着 owner(池里的行必须无人认领)。"
|
|
69
|
-
)
|
|
70
|
-
if task.get("status") not in STATUSES:
|
|
71
|
-
problems.append(f"公共池里的 {tid} 状态 {task.get('status')!r} 非法。")
|
|
72
|
-
|
|
73
|
-
for key, path in index.shards().items():
|
|
74
|
-
if not path.exists():
|
|
75
|
-
problems.append(f"索引登记了 {key},但分片文件 {path.name} 不存在。")
|
|
76
|
-
continue
|
|
77
|
-
checked += 1
|
|
78
|
-
store = TaskStore(repo, Path(path).relative_to(repo / "ledger"))
|
|
79
|
-
for tid, task in _read_tasks(repo, store, problems).items():
|
|
80
|
-
total += 1
|
|
81
|
-
owner = owner_of(task)
|
|
82
|
-
if not owner:
|
|
83
|
-
problems.append(f"分片 {key} 里的 {tid} 没有 owner。")
|
|
84
|
-
elif owner != key:
|
|
85
|
-
problems.append(
|
|
86
|
-
f"分片 {key} 里的 {tid} 的 owner 是 {owner}(分片主人与分区键不一致)。"
|
|
87
|
-
)
|
|
88
|
-
status = status_of(task)
|
|
89
|
-
if status not in STATUSES:
|
|
90
|
-
problems.append(f"分片 {key} 里的 {tid} 状态 {status!r} 非法。")
|
|
91
|
-
|
|
92
|
-
return checked, total
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _check_orphan_shards(index: OwnersIndex, problems: list[str]) -> None:
|
|
96
|
-
"""目录里有、索引没登记的分片(全仓唯一扫目录的地方)。"""
|
|
97
|
-
registered = {p.name for p in index.shards().values()}
|
|
98
|
-
for path in index.disk_shards():
|
|
99
|
-
if path.name not in registered:
|
|
100
|
-
problems.append(
|
|
101
|
-
f"owners/ 里的 {path.name} 没有在索引里登记(对它人等于不存在)。"
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _history_paths(store: HistoryStore, *, deep: bool) -> list[Path]:
|
|
106
|
-
"""要读的历史文件:浅度只读当月那个月文件(存量平铺文件不读),深度读全部。"""
|
|
107
|
-
sources = store.sources()
|
|
108
|
-
if deep:
|
|
109
|
-
return sources
|
|
110
|
-
month = now()[0:4] + now()[5:7]
|
|
111
|
-
return [p for p in sources if store.month_key(p) == month]
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _check_history(
|
|
115
|
-
repo: Path,
|
|
116
|
-
paths: list[Path],
|
|
117
|
-
open_docs: set[str],
|
|
118
|
-
store: HistoryStore,
|
|
119
|
-
notices: list[str],
|
|
120
|
-
) -> int:
|
|
121
|
-
"""历史文件的格式与对账:解析警告、行尾文档链接对得上在办任务没。"""
|
|
122
|
-
checked = 0
|
|
123
|
-
for path in paths:
|
|
124
|
-
checked += 1
|
|
125
|
-
rows, warns = history_md.parse_history(store.read(path))
|
|
126
|
-
notices.extend(f"{rel(repo, path)}:{w}" for w in warns)
|
|
127
|
-
for row in rows:
|
|
128
|
-
name = Path(row.get("worklog") or "").name
|
|
129
|
-
if name and name in open_docs:
|
|
130
|
-
notices.append(
|
|
131
|
-
f"历史里有「{row.get('title') or name}」的结案行,"
|
|
132
|
-
"但它还挂在在办台账里。"
|
|
133
|
-
)
|
|
134
|
-
return checked
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def validate(repo: Path, me: Identity, deep: bool = False) -> ValidationReport:
|
|
138
|
-
problems: list[str] = []
|
|
139
|
-
notices: list[str] = []
|
|
140
|
-
checked = 0
|
|
141
|
-
total = 0
|
|
142
|
-
|
|
143
|
-
index = OwnersIndex(repo)
|
|
144
|
-
history = HistoryStore(repo)
|
|
145
|
-
|
|
146
|
-
files, tasks = _check_task_files(repo, index, problems)
|
|
147
|
-
checked += files
|
|
148
|
-
total += tasks
|
|
149
|
-
_check_orphan_shards(index, problems)
|
|
150
|
-
|
|
151
|
-
checked += _check_history(
|
|
152
|
-
repo,
|
|
153
|
-
_history_paths(history, deep=deep),
|
|
154
|
-
_open_worklog_names(repo, index),
|
|
155
|
-
history,
|
|
156
|
-
notices,
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
if not (repo / to_repo_rel(OWNERS_REL)).exists():
|
|
160
|
-
notices.append("还没有 owners.json(索引):跑一条写命令会自动建。")
|
|
161
|
-
stray = repo / LEDGER_DIR / LEDGER_DIR
|
|
162
|
-
if stray.is_dir():
|
|
163
|
-
problems.append(
|
|
164
|
-
f"{rel(repo, stray)} 是错位的台账目录(根被当成 ledger/ 时写出来的幻影);"
|
|
165
|
-
"确认哪份是真台账后删掉多余的,命令一律带 --repo <工作区根>。"
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
return ValidationReport(
|
|
169
|
-
problems=tuple(problems),
|
|
170
|
-
notices=tuple(notices),
|
|
171
|
-
checked_files=checked,
|
|
172
|
-
total_tasks=total,
|
|
173
|
-
deep=deep,
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _open_worklog_names(repo: Path, index: OwnersIndex) -> set[str]:
|
|
178
|
-
"""在办任务的过程文档**文件名**集合(池 + 全部分片):历史行按它对账。"""
|
|
179
|
-
stores = [TaskStore(repo, POOL_REL)]
|
|
180
|
-
for path in index.shards().values():
|
|
181
|
-
if path.exists():
|
|
182
|
-
stores.append(TaskStore(repo, Path(path).relative_to(repo / "ledger")))
|
|
183
|
-
out: set[str] = set()
|
|
184
|
-
for store in stores:
|
|
185
|
-
for task in store.all().values():
|
|
186
|
-
name = Path((task.get("worklog") or "").strip()).name
|
|
187
|
-
if name:
|
|
188
|
-
out.add(name)
|
|
189
|
-
return out
|
|
File without changes
|