@sakanano-moe/agenthelm 6.7.0 → 6.11.0-rc.1
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/package.json +1 -1
- package/runtime/index.json +6 -6
- package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/agent-pack/hooks/hooks.json +32 -0
- package/runtime/plugins/agent-pack/plugin.json +1 -1
- package/runtime/plugins/agent-pack/references/local-only.md +126 -0
- package/runtime/plugins/agent-pack/references/nudge.en.md +58 -0
- package/runtime/plugins/agent-pack/references/nudge.md +58 -0
- package/runtime/plugins/agent-pack/runtime/agent_pack_nudge.py +573 -0
- package/runtime/plugins/agent-pack/runtime/agenthelm.py +419 -107
- package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +77 -53
- package/runtime/plugins/agent-pack/runtime/i18n_agent_pack.py +78 -0
- package/runtime/plugins/agent-pack/runtime/local_only.py +1130 -0
- package/runtime/plugins/agent-pack/runtime/locale/en.json +385 -0
- package/runtime/plugins/agent-pack/runtime/mcp_server.py +25 -0
- package/runtime/plugins/agent-pack/runtime/nudge_text.py +107 -0
- package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +9 -1
- package/runtime/plugins/agents-init/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/agents-init/content.json +17 -2
- package/runtime/plugins/agents-init/plugin.json +1 -1
- package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +47 -0
- package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +39 -1
- package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +404 -70
- package/runtime/plugins/agents-init/skills/agents-init/scripts/adopt.py +596 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/i18n_init.py +78 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +245 -72
- package/runtime/plugins/agents-init/skills/agents-init/scripts/locale/en.json +210 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/regions.py +99 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/scaffold.py +1081 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_adopt.py +551 -0
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +46 -2
- package/runtime/plugins/agents-init/skills/agents-init/scripts/test_scaffold.py +555 -0
- package/runtime/plugins/agents-init/skills/agents-init/skeleton.json +66 -0
- package/runtime/plugins/agents-init/skills/agents-init/skill.json +1 -1
- package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/plugin.json +1 -1
- package/runtime/plugins/opinion-manager/references/inbox.en.md +48 -0
- package/runtime/plugins/opinion-manager/references/inbox.md +48 -0
- package/runtime/plugins/opinion-manager/runtime/context.py +19 -18
- package/runtime/plugins/opinion-manager/runtime/i18n_opinion.py +78 -0
- package/runtime/plugins/opinion-manager/runtime/inbox.py +397 -0
- package/runtime/plugins/opinion-manager/runtime/inbox_text.py +101 -0
- package/runtime/plugins/opinion-manager/runtime/lifecycle.py +45 -44
- package/runtime/plugins/opinion-manager/runtime/locale/en.json +184 -0
- package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +55 -50
- package/runtime/plugins/opinion-manager/runtime/versioned.py +84 -83
- package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +15 -2
- package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/content.json +30 -3
- package/runtime/plugins/project-orchestrator/plugin.json +1 -1
- package/runtime/plugins/project-orchestrator/references/case-workspaces.md +9 -1
- package/runtime/plugins/project-orchestrator/references/case.schema.json +1 -0
- package/runtime/plugins/project-orchestrator/references/sop-interview.md +83 -0
- package/runtime/plugins/project-orchestrator/references/sop.md +156 -0
- package/runtime/plugins/project-orchestrator/runtime/assets.py +52 -57
- package/runtime/plugins/project-orchestrator/runtime/case.py +139 -80
- package/runtime/plugins/project-orchestrator/runtime/case_structure.py +71 -0
- package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +47 -46
- package/runtime/plugins/project-orchestrator/runtime/i18n_orchestrator.py +78 -0
- package/runtime/plugins/project-orchestrator/runtime/locale/en.json +1409 -0
- package/runtime/plugins/project-orchestrator/runtime/memory.py +44 -43
- package/runtime/plugins/project-orchestrator/runtime/private_paths.py +119 -0
- package/runtime/plugins/project-orchestrator/runtime/project_base.py +37 -19
- package/runtime/plugins/project-orchestrator/runtime/project_cli.py +267 -174
- package/runtime/plugins/project-orchestrator/runtime/project_demo.py +130 -48
- package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +524 -134
- package/runtime/plugins/project-orchestrator/runtime/project_menu.py +159 -85
- package/runtime/plugins/project-orchestrator/runtime/project_real.py +191 -106
- package/runtime/plugins/project-orchestrator/runtime/project_write.py +58 -55
- package/runtime/plugins/project-orchestrator/runtime/sop.py +1670 -0
- package/runtime/plugins/project-orchestrator/runtime/sop_structure.py +226 -0
- package/runtime/plugins/project-orchestrator/runtime/wf.py +74 -70
- package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +22 -0
- package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.en.md +57 -0
- package/runtime/plugins/project-orchestrator/sop-templates/blank/SOP.md +57 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.en.md +77 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/SOP.md +77 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.en.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/knowledge-digest/templates/digest.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.en.md +87 -0
- package/runtime/plugins/project-orchestrator/sop-templates/video-to-transcript/SOP.md +87 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.en.md +75 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/SOP.md +75 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.en.md +11 -0
- package/runtime/plugins/project-orchestrator/sop-templates/weekly-review/templates/review.md +12 -0
- package/runtime/plugins/project-orchestrator/templates/CASE.en.md +39 -0
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
扫描范围来自 Project Profile:Case 根取 ``work.case.root``(缺省 ``cases``),额外扫描根取
|
|
9
9
|
``work.assets.roots``(缺省无)。同义词来自项目可选文件
|
|
10
10
|
``.agents/moe.sakanano.agent-pack/asset-synonyms.json``(缺省为空)。凭据、密钥与证书
|
|
11
|
-
无论是否入库都不登记;嵌套的 SVN/Git
|
|
11
|
+
无论是否入库都不登记;嵌套的 SVN/Git 外部检出与纯缓存目录不展开。Case 的 sensitivity 不影响登记:台账只是本机缓存里的索引,
|
|
12
|
+
private 的 Case 也能被找到(读取其内容前是否需要确认,由私有目录的约定管,不在这里过滤)。
|
|
12
13
|
"""
|
|
13
14
|
from __future__ import annotations
|
|
14
15
|
|
|
@@ -26,6 +27,7 @@ import time
|
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
from typing import Any, Iterator
|
|
28
29
|
from urllib.parse import unquote
|
|
30
|
+
from i18n_orchestrator import _ # noqa: E402
|
|
29
31
|
|
|
30
32
|
try:
|
|
31
33
|
import fcntl
|
|
@@ -34,7 +36,7 @@ except ImportError: # Windows
|
|
|
34
36
|
|
|
35
37
|
PROFILE = Path(".agents/moe.sakanano.agent-pack/project.json")
|
|
36
38
|
SYNONYMS = Path(".agents/moe.sakanano.agent-pack/asset-synonyms.json")
|
|
37
|
-
ASSET_SCHEMA_VERSION =
|
|
39
|
+
ASSET_SCHEMA_VERSION = 2 # 2: 非 shared 的 Case 也登记;旧台账自动重建
|
|
38
40
|
DB_NAME = "assets.sqlite3"
|
|
39
41
|
LOCK_NAME = "assets.lock"
|
|
40
42
|
LAST_NAME = "wf-last.json"
|
|
@@ -114,7 +116,7 @@ class Project:
|
|
|
114
116
|
def __init__(self, root: Path) -> None:
|
|
115
117
|
self.root = root.expanduser().resolve()
|
|
116
118
|
if not self.root.is_dir():
|
|
117
|
-
raise AssetError(
|
|
119
|
+
raise AssetError(_("项目目录不存在:{0}").format(root), "project-missing")
|
|
118
120
|
profile_path = self.root / PROFILE
|
|
119
121
|
self.profile_found = profile_path.is_file()
|
|
120
122
|
profile: dict = {}
|
|
@@ -122,7 +124,7 @@ class Project:
|
|
|
122
124
|
try:
|
|
123
125
|
profile = json.loads(profile_path.read_text(encoding="utf-8"))
|
|
124
126
|
except (OSError, ValueError) as exc:
|
|
125
|
-
raise AssetError(
|
|
127
|
+
raise AssetError(_("Project Profile 无法解析:{0}").format(exc), "profile-invalid") from exc
|
|
126
128
|
work = profile.get("work") if isinstance(profile, dict) else None
|
|
127
129
|
work = work if isinstance(work, dict) else {}
|
|
128
130
|
case = work.get("case") if isinstance(work.get("case"), dict) else {}
|
|
@@ -130,7 +132,7 @@ class Project:
|
|
|
130
132
|
self.case_root = self._relative(case.get("root") or "cases", "work.case.root")
|
|
131
133
|
extra = assets.get("roots") or []
|
|
132
134
|
if not isinstance(extra, list) or not all(isinstance(item, str) for item in extra):
|
|
133
|
-
raise AssetError("work.assets.roots 必须是字符串列表", "profile-invalid")
|
|
135
|
+
raise AssetError(_("work.assets.roots 必须是字符串列表"), "profile-invalid")
|
|
134
136
|
roots = [self.case_root]
|
|
135
137
|
for value in extra:
|
|
136
138
|
relative = self._relative(value, "work.assets.roots")
|
|
@@ -139,7 +141,7 @@ class Project:
|
|
|
139
141
|
for first in roots:
|
|
140
142
|
for second in roots:
|
|
141
143
|
if first != second and second.startswith(first + "/"):
|
|
142
|
-
raise AssetError(
|
|
144
|
+
raise AssetError(_("扫描根互相嵌套:{0} 与 {1}").format(first, second), "profile-invalid")
|
|
143
145
|
self.roots = roots
|
|
144
146
|
|
|
145
147
|
@classmethod
|
|
@@ -154,15 +156,15 @@ class Project:
|
|
|
154
156
|
|
|
155
157
|
def _relative(self, value: Any, label: str) -> str:
|
|
156
158
|
if not isinstance(value, str):
|
|
157
|
-
raise AssetError(
|
|
159
|
+
raise AssetError(_("{0} 必须是项目内相对路径").format(label), "profile-invalid")
|
|
158
160
|
relative = Path(value)
|
|
159
161
|
if relative.is_absolute() or ".." in relative.parts or not relative.parts:
|
|
160
|
-
raise AssetError(
|
|
162
|
+
raise AssetError(_("{0} 必须是项目内相对路径").format(label), "profile-invalid")
|
|
161
163
|
current = self.root
|
|
162
164
|
for part in relative.parts:
|
|
163
165
|
current = current / part
|
|
164
166
|
if current.is_symlink():
|
|
165
|
-
raise AssetError(
|
|
167
|
+
raise AssetError(_("{0} 不能经过符号链接").format(label), "symlink")
|
|
166
168
|
return relative.as_posix()
|
|
167
169
|
|
|
168
170
|
def state_dir(self, create: bool = False) -> Path:
|
|
@@ -172,7 +174,7 @@ class Project:
|
|
|
172
174
|
directory = cache / key
|
|
173
175
|
resolved = directory.resolve()
|
|
174
176
|
if resolved == self.root or self.root in resolved.parents:
|
|
175
|
-
raise AssetError("缓存目录不能位于项目内(检查 XDG_CACHE_HOME)", "state-inside-project")
|
|
177
|
+
raise AssetError(_("缓存目录不能位于项目内(检查 XDG_CACHE_HOME)"), "state-inside-project")
|
|
176
178
|
if create:
|
|
177
179
|
directory.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
178
180
|
try:
|
|
@@ -181,7 +183,7 @@ class Project:
|
|
|
181
183
|
pass
|
|
182
184
|
for path in (directory, directory / DB_NAME, directory / LOCK_NAME, directory / LAST_NAME):
|
|
183
185
|
if path.is_symlink():
|
|
184
|
-
raise AssetError(
|
|
186
|
+
raise AssetError(_("缓存路径不能是符号链接:{0}").format(path.name), "symlink")
|
|
185
187
|
return directory
|
|
186
188
|
|
|
187
189
|
def synonyms(self) -> list[list[str]]:
|
|
@@ -191,10 +193,10 @@ class Project:
|
|
|
191
193
|
try:
|
|
192
194
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
193
195
|
except (OSError, ValueError) as exc:
|
|
194
|
-
raise AssetError(
|
|
196
|
+
raise AssetError(_("同义词文件无法解析:{0}").format(exc), "synonyms-invalid") from exc
|
|
195
197
|
groups = data.get("groups") if isinstance(data, dict) else None
|
|
196
198
|
if not isinstance(groups, list):
|
|
197
|
-
raise AssetError("同义词文件需要 groups 列表", "synonyms-invalid")
|
|
199
|
+
raise AssetError(_("同义词文件需要 groups 列表"), "synonyms-invalid")
|
|
198
200
|
return [[str(word).lower() for word in group if str(word).strip()]
|
|
199
201
|
for group in groups if isinstance(group, list) and len(group) > 1]
|
|
200
202
|
|
|
@@ -535,21 +537,17 @@ def scan(project: Project, previous: dict | None = None) -> dict:
|
|
|
535
537
|
previous = previous or {}
|
|
536
538
|
root = project.root
|
|
537
539
|
containers = _load_containers(project)
|
|
538
|
-
non_shared = {cid for cid, item in containers.items() if item["sensitivity"] != "shared"}
|
|
539
540
|
skipped: list[dict] = []
|
|
540
541
|
checkouts: list[str] = []
|
|
541
542
|
raw = []
|
|
542
543
|
for rel, stat in _walk(project, skipped, checkouts):
|
|
543
|
-
container, kind, _ = project.container_of(rel)
|
|
544
|
-
if kind == "case" and container in non_shared:
|
|
545
|
-
continue
|
|
546
544
|
raw.append((rel, stat))
|
|
547
545
|
external = _checkout_roots(project, checkouts)
|
|
548
546
|
for directory in sorted(external):
|
|
549
547
|
skipped.append({"reason": "external-checkout", "path": directory})
|
|
550
548
|
if external:
|
|
551
549
|
raw = [(rel, stat) for rel, stat in raw if not any(rel.startswith(item + "/") for item in external)]
|
|
552
|
-
has_git, tracked, last_commit, dirty, ignored = _git_state(project, [rel for rel,
|
|
550
|
+
has_git, tracked, last_commit, dirty, ignored = _git_state(project, [rel for rel, _stat in raw])
|
|
553
551
|
|
|
554
552
|
files = {}
|
|
555
553
|
denied = 0
|
|
@@ -598,7 +596,7 @@ def scan(project: Project, previous: dict | None = None) -> dict:
|
|
|
598
596
|
}
|
|
599
597
|
edges, broken = _extract_edges(project, files, containers)
|
|
600
598
|
return {"files": files, "containers": containers, "edges": edges, "broken": broken,
|
|
601
|
-
"
|
|
599
|
+
"skipped": skipped, "denied": denied, "git": has_git}
|
|
602
600
|
|
|
603
601
|
|
|
604
602
|
def _prefixes(project: Project) -> tuple[str, ...]:
|
|
@@ -721,7 +719,7 @@ def _extract_edges(project: Project, files: dict, containers: dict) -> tuple[dic
|
|
|
721
719
|
elif looks and match.group(1) not in seen:
|
|
722
720
|
broken.setdefault((src, match.group(1)), "link")
|
|
723
721
|
for match in WIKI.finditer(text):
|
|
724
|
-
dst,
|
|
722
|
+
dst, _kind = resolve(match.group(1), src, "wiki")
|
|
725
723
|
if dst:
|
|
726
724
|
add_edge(src, dst, "wikilink", match.group(1))
|
|
727
725
|
else:
|
|
@@ -784,7 +782,7 @@ class _Lock:
|
|
|
784
782
|
try:
|
|
785
783
|
self.fd = os.open(self.path, flags, 0o600)
|
|
786
784
|
except OSError as exc:
|
|
787
|
-
raise AssetError(
|
|
785
|
+
raise AssetError(_("无法打开台账锁:{0}").format(exc), "state-lock") from exc
|
|
788
786
|
try:
|
|
789
787
|
if fcntl is not None:
|
|
790
788
|
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
@@ -794,7 +792,7 @@ class _Lock:
|
|
|
794
792
|
except OSError as exc:
|
|
795
793
|
os.close(self.fd)
|
|
796
794
|
self.fd = None
|
|
797
|
-
raise AssetError("台账正在被另一个进程重建,请稍后重试", "index-locked") from exc
|
|
795
|
+
raise AssetError(_("台账正在被另一个进程重建,请稍后重试"), "index-locked") from exc
|
|
798
796
|
return self
|
|
799
797
|
|
|
800
798
|
def __exit__(self, *_: Any) -> None:
|
|
@@ -828,7 +826,6 @@ def _write_database(path: Path, registry: dict, fingerprint: str, project: Proje
|
|
|
828
826
|
"project": str(project.root), "roots": json.dumps(project.roots, ensure_ascii=False),
|
|
829
827
|
"file_count": str(len(registry["files"])), "edge_count": str(len(registry["edges"])),
|
|
830
828
|
"denied": str(registry["denied"]), "git": "1" if registry["git"] else "0",
|
|
831
|
-
"non_shared": json.dumps(registry["non_shared"]),
|
|
832
829
|
"skipped": json.dumps(registry["skipped"], ensure_ascii=False)}
|
|
833
830
|
connection.executemany("INSERT INTO meta VALUES (?, ?)", meta.items())
|
|
834
831
|
connection.executemany(
|
|
@@ -860,16 +857,16 @@ def _write_database(path: Path, registry: dict, fingerprint: str, project: Proje
|
|
|
860
857
|
def _open(project: Project) -> sqlite3.Connection:
|
|
861
858
|
path = project.state_dir() / DB_NAME
|
|
862
859
|
if not path.is_file():
|
|
863
|
-
raise AssetError("台账尚未建立,先运行 rebuild(或去掉 --no-refresh)", "index-missing")
|
|
860
|
+
raise AssetError(_("台账尚未建立,先运行 rebuild(或去掉 --no-refresh)"), "index-missing")
|
|
864
861
|
try:
|
|
865
862
|
connection = sqlite3.connect(f"{path.as_uri()}?mode=ro", uri=True)
|
|
866
863
|
connection.row_factory = sqlite3.Row
|
|
867
864
|
version = connection.execute("PRAGMA user_version").fetchone()[0]
|
|
868
865
|
except sqlite3.Error as exc:
|
|
869
|
-
raise AssetError("台账无法读取,请运行 rebuild --force", "index-corrupt") from exc
|
|
866
|
+
raise AssetError(_("台账无法读取,请运行 rebuild --force"), "index-corrupt") from exc
|
|
870
867
|
if version != ASSET_SCHEMA_VERSION:
|
|
871
868
|
connection.close()
|
|
872
|
-
raise AssetError("台账格式与当前工具不一致,请运行 rebuild --force", "index-incompatible")
|
|
869
|
+
raise AssetError(_("台账格式与当前工具不一致,请运行 rebuild --force"), "index-incompatible")
|
|
873
870
|
return connection
|
|
874
871
|
|
|
875
872
|
|
|
@@ -935,6 +932,7 @@ def _row_info(row: sqlite3.Row, info: dict, inbound: collections.Counter) -> dic
|
|
|
935
932
|
return {"path": row["path"], "name": row["name"], "title": row["title"], "role": row["role"],
|
|
936
933
|
"container": row["container"], "container_kind": row["container_kind"],
|
|
937
934
|
"case_title": container.get("title", ""), "status": container.get("status", ""),
|
|
935
|
+
"sensitivity": container.get("sensitivity", "shared"),
|
|
938
936
|
"modified": _iso(row["mtime_ns"] / 1e9), "size": row["size"],
|
|
939
937
|
"git": row["git_state"] + ("+dirty" if row["git_dirty"] else ""),
|
|
940
938
|
"dataless": bool(row["dataless"]), "referenced_by": inbound.get(row["path"], 0)}
|
|
@@ -1025,7 +1023,7 @@ def recent(project: Project, *, limit: int = 15, refresh: bool = True) -> dict:
|
|
|
1025
1023
|
info, inbound = _containers(connection), _inbound_counts(connection)
|
|
1026
1024
|
rows = connection.execute("SELECT * FROM files WHERE role IN ('deliverable','draft','project-file') "
|
|
1027
1025
|
"ORDER BY mtime_ns DESC LIMIT 400").fetchall()
|
|
1028
|
-
groups,
|
|
1026
|
+
groups, _total = _group_families([(0, row["mtime_ns"], row) for row in rows], info, inbound, limit)
|
|
1029
1027
|
return {"ok": True, "results": groups, "returned": len(groups)}
|
|
1030
1028
|
finally:
|
|
1031
1029
|
connection.close()
|
|
@@ -1040,7 +1038,7 @@ def cases(project: Project, term: str | None = None, *, limit: int = 30, refresh
|
|
|
1040
1038
|
for row in connection.execute("SELECT * FROM containers WHERE kind = 'case'"):
|
|
1041
1039
|
haystack = " ".join([row["id"], row["title"], row["goal"], row["tags"], row["aliases"]]).lower()
|
|
1042
1040
|
if term:
|
|
1043
|
-
words,
|
|
1041
|
+
words, _dates = parse_query([term])
|
|
1044
1042
|
if not all(any(word in haystack for word in alternatives)
|
|
1045
1043
|
for alternatives in expand_terms(words, groups)):
|
|
1046
1044
|
continue
|
|
@@ -1073,7 +1071,7 @@ def refs(project: Project, path: str, *, depth: int = 1, refresh: bool = True) -
|
|
|
1073
1071
|
if not connection.execute("SELECT 1 FROM files WHERE path = ?", (path,)).fetchone() and \
|
|
1074
1072
|
not connection.execute("SELECT 1 FROM edges WHERE dst = ? OR dst = ?",
|
|
1075
1073
|
(path, path.rstrip("/") + "/")).fetchone():
|
|
1076
|
-
raise AssetError(
|
|
1074
|
+
raise AssetError(_("台账里没有这个路径:{0}").format(path), "unknown-path")
|
|
1077
1075
|
outbound = [{"dst": row["dst"], "type": row["type"]}
|
|
1078
1076
|
for row in connection.execute("SELECT dst, type FROM edges WHERE src = ? ORDER BY dst", (path,))]
|
|
1079
1077
|
direct, seen_src = [], set()
|
|
@@ -1083,7 +1081,7 @@ def refs(project: Project, path: str, *, depth: int = 1, refresh: bool = True) -
|
|
|
1083
1081
|
direct.append(row)
|
|
1084
1082
|
layers, seen = [direct], {path} | seen_src
|
|
1085
1083
|
frontier = [row["src"] for row in direct]
|
|
1086
|
-
for
|
|
1084
|
+
for _depth in range(max(depth, 1) - 1):
|
|
1087
1085
|
following = []
|
|
1088
1086
|
for node in frontier:
|
|
1089
1087
|
for row in _inbound_rows(connection, node):
|
|
@@ -1175,7 +1173,7 @@ def check(project: Project, *, limit: int = 50, refresh: bool = True) -> dict:
|
|
|
1175
1173
|
# ---------------------------------------------------------------- cli
|
|
1176
1174
|
|
|
1177
1175
|
def parser() -> argparse.ArgumentParser:
|
|
1178
|
-
result = argparse.ArgumentParser(description=
|
|
1176
|
+
result = argparse.ArgumentParser(description=_("交付物台账:登记 Case 与额外扫描根下的文件,按标题、日期、同义词查找,追踪版本与引用。"))
|
|
1179
1177
|
result.add_argument("--project", type=Path, default=None)
|
|
1180
1178
|
result.add_argument("--output", choices=("text", "json"), default="text")
|
|
1181
1179
|
# The same options are accepted after the subcommand, where people tend to type them.
|
|
@@ -1187,28 +1185,28 @@ def parser() -> argparse.ArgumentParser:
|
|
|
1187
1185
|
def command(name: str, help_text: str, refresh: bool = True) -> argparse.ArgumentParser:
|
|
1188
1186
|
item = sub.add_parser(name, parents=[shared], help=help_text)
|
|
1189
1187
|
if refresh:
|
|
1190
|
-
item.add_argument("--no-refresh", action="store_true", help="不先增量重建,直接读现有台账")
|
|
1188
|
+
item.add_argument("--no-refresh", action="store_true", help=_("不先增量重建,直接读现有台账"))
|
|
1191
1189
|
return item
|
|
1192
1190
|
|
|
1193
|
-
build = command("rebuild", "重建台账(文件清单未变时为 no-op)", refresh=False)
|
|
1191
|
+
build = command("rebuild", _("重建台账(文件清单未变时为 no-op)"), refresh=False)
|
|
1194
1192
|
build.add_argument("--force", action="store_true")
|
|
1195
|
-
command("status", "台账是否最新、位置与扫描根", refresh=False)
|
|
1196
|
-
search = command("find", "按标题、文件名、日期与同义词查找,同一系列的版本归为一组")
|
|
1193
|
+
command("status", _("台账是否最新、位置与扫描根"), refresh=False)
|
|
1194
|
+
search = command("find", _("按标题、文件名、日期与同义词查找,同一系列的版本归为一组"))
|
|
1197
1195
|
search.add_argument("terms", nargs="*")
|
|
1198
|
-
search.add_argument("--kind", help="角色:deliverable / draft / source / data / image / script / process / project-file")
|
|
1196
|
+
search.add_argument("--kind", help=_("角色:deliverable / draft / source / data / image / script / process / project-file"))
|
|
1199
1197
|
search.add_argument("--ext")
|
|
1200
|
-
search.add_argument("--in", dest="container", help="限定 Case 或目录(片段匹配)")
|
|
1201
|
-
search.add_argument("--content", action="store_true", help="名称不命中时再查文本正文(≤1 MiB)")
|
|
1198
|
+
search.add_argument("--in", dest="container", help=_("限定 Case 或目录(片段匹配)"))
|
|
1199
|
+
search.add_argument("--content", action="store_true", help=_("名称不命中时再查文本正文(≤1 MiB)"))
|
|
1202
1200
|
search.add_argument("--limit", type=int, default=10)
|
|
1203
|
-
latest = command("recent", "最近改动的交付物")
|
|
1201
|
+
latest = command("recent", _("最近改动的交付物"))
|
|
1204
1202
|
latest.add_argument("--limit", type=int, default=15)
|
|
1205
|
-
listing = command("cases", "Case 列表(进行中在前)与各自登记的文件数")
|
|
1203
|
+
listing = command("cases", _("Case 列表(进行中在前)与各自登记的文件数"))
|
|
1206
1204
|
listing.add_argument("term", nargs="?")
|
|
1207
1205
|
listing.add_argument("--limit", type=int, default=30)
|
|
1208
|
-
reference = command("refs", "谁引用它、它引用谁、改动会波及哪些 Case")
|
|
1206
|
+
reference = command("refs", _("谁引用它、它引用谁、改动会波及哪些 Case"))
|
|
1209
1207
|
reference.add_argument("path")
|
|
1210
1208
|
reference.add_argument("--depth", type=int, default=1)
|
|
1211
|
-
inspect = command("check", "巡检:无人认领的交付物、断链、跨容器重复、上游更新")
|
|
1209
|
+
inspect = command("check", _("巡检:无人认领的交付物、断链、跨容器重复、上游更新"))
|
|
1212
1210
|
inspect.add_argument("--limit", type=int, default=50)
|
|
1213
1211
|
return result
|
|
1214
1212
|
|
|
@@ -1219,32 +1217,29 @@ def render_text(command: str, result: dict) -> str:
|
|
|
1219
1217
|
for index, item in enumerate(result["results"], 1):
|
|
1220
1218
|
owner = item["case_title"] or item["container"]
|
|
1221
1219
|
lines.append(f"{index:>2}. {item['title'] or item['name']} [{owner}] {item['git']} {item['path']}")
|
|
1222
|
-
lines += [
|
|
1223
|
-
return "\n".join(lines) or "没有找到。"
|
|
1220
|
+
lines += [_(" 版本 {0} {1}").format(version['name'], version['path']) for version in item["versions"]]
|
|
1221
|
+
return "\n".join(lines) or _("没有找到。")
|
|
1224
1222
|
if command == "cases":
|
|
1225
|
-
return "\n".join(
|
|
1226
|
-
for item in result["results"]) or "没有 Case。"
|
|
1223
|
+
return "\n".join(_("{0:9} {1} {2} ({3} 个文件)").format(item['status'] or '-', item['id'], item['title'], item['files'])
|
|
1224
|
+
for item in result["results"]) or _("没有 Case。")
|
|
1227
1225
|
if command == "refs":
|
|
1228
1226
|
lines = [result["path"]]
|
|
1229
1227
|
lines += [f" ← {item['src']}" + ("" if item["via"] == "file" else f" ({item['via']})")
|
|
1230
1228
|
for item in result["inbound"]]
|
|
1231
1229
|
lines += [f" → {item['dst']}" for item in result["outbound"]]
|
|
1232
1230
|
if result["affected_containers"]:
|
|
1233
|
-
lines.append(" 改动会波及:" + "、".join(result["affected_containers"]))
|
|
1231
|
+
lines.append(_(" 改动会波及:") + _("、").join(result["affected_containers"]))
|
|
1234
1232
|
return "\n".join(lines)
|
|
1235
1233
|
if command == "check":
|
|
1236
1234
|
totals, tiers, counts = result["totals"], result["claim_tiers"], result["broken_counts"]
|
|
1237
1235
|
git = totals["git"]
|
|
1238
|
-
lines = [
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
lines += [f" 断链 {item['src']} → {item['raw']}" for item in result["broken"].get("real-broken", [])]
|
|
1246
|
-
lines += [f" 无人认领 {path}" for paths in result["unclaimed"].values() for path in paths]
|
|
1247
|
-
lines += [" 重复 " + " ≡ ".join(group) for group in result["duplicates"]]
|
|
1236
|
+
lines = [_("文件 {0} · 引用 {1} · 拒绝登记的凭据类文件 {2}").format(totals['files'], totals['edges'], totals['denied']),
|
|
1237
|
+
_("Git:已入库 {0} / 被忽略 {1} / 未提交 {2} / 非 Git {3}").format(git.get('tracked', 0), git.get('ignored', 0), git.get('untracked', 0), git.get('none', 0)),
|
|
1238
|
+
_("交付物:文件级被引用 {0} · 仅目录覆盖 {1} · 无人认领 {2}").format(tiers.get('file', 0), tiers.get('directory', 0), tiers.get('none', 0)),
|
|
1239
|
+
_("断链:{0} · 说明文字 {1} · 旧 Task 路径 {2} · 主题链接 {3}").format(counts.get('real-broken', 0), counts.get('prose-not-path', 0), counts.get('legacy-task-id', 0), counts.get('wikilink-topic', 0))]
|
|
1240
|
+
lines += [_(" 断链 {0} → {1}").format(item['src'], item['raw']) for item in result["broken"].get("real-broken", [])]
|
|
1241
|
+
lines += [_(" 无人认领 {0}").format(path) for paths in result["unclaimed"].values() for path in paths]
|
|
1242
|
+
lines += [_(" 重复 ") + " ≡ ".join(group) for group in result["duplicates"]]
|
|
1248
1243
|
return "\n".join(lines)
|
|
1249
1244
|
return json.dumps(result, ensure_ascii=False, indent=2)
|
|
1250
1245
|
|