specmodule 0.1.0__py3-none-any.whl
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.
- llm/__init__.py +21 -0
- llm/client.py +654 -0
- llm/config.py +213 -0
- module_harness/__init__.py +201 -0
- module_harness/align.py +39 -0
- module_harness/builtins.py +29 -0
- module_harness/checkpoint.py +336 -0
- module_harness/cli.py +1492 -0
- module_harness/command.py +115 -0
- module_harness/config.py +95 -0
- module_harness/consistency.py +123 -0
- module_harness/entry.py +74 -0
- module_harness/events.py +149 -0
- module_harness/feed.py +197 -0
- module_harness/graph_builder.py +334 -0
- module_harness/harness.py +181 -0
- module_harness/loader.py +215 -0
- module_harness/module.py +452 -0
- module_harness/outputfmt.py +139 -0
- module_harness/prompt.py +84 -0
- module_harness/query.py +216 -0
- module_harness/registry.py +180 -0
- module_harness/scaffold.py +404 -0
- module_harness/spec.py +209 -0
- module_harness/status.py +96 -0
- module_harness/store.py +482 -0
- module_harness/submodule.py +268 -0
- module_harness/templates/builtin/codereview.json +32 -0
- module_harness/templates/builtin/docwrite.json +30 -0
- module_harness/templates/builtin/summarize.json +24 -0
- module_harness/templates/builtin/translate.json +27 -0
- module_harness/translator.py +314 -0
- specmodule-0.1.0.dist-info/METADATA +321 -0
- specmodule-0.1.0.dist-info/RECORD +38 -0
- specmodule-0.1.0.dist-info/WHEEL +5 -0
- specmodule-0.1.0.dist-info/entry_points.txt +2 -0
- specmodule-0.1.0.dist-info/licenses/LICENSE +21 -0
- specmodule-0.1.0.dist-info/top_level.txt +2 -0
module_harness/query.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# module_harness/query.py
|
|
2
|
+
"""共享查询层:review 历史时间线组合(roadmap Phase 0)。
|
|
3
|
+
|
|
4
|
+
CLI(host + 查询形态)、MCP、Web 三形态共同消费本模块——形态只 import,
|
|
5
|
+
绝不重实现。数据源:run.sqlite 的 firings 表;容错哲学同 query_run_status
|
|
6
|
+
(DB 读失败返回 None,监控方绝不被 DB 锁搞崩)。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ReviewEntry:
|
|
21
|
+
"""单个节点 firing 的审阅记录。"""
|
|
22
|
+
|
|
23
|
+
tick: int
|
|
24
|
+
node: str
|
|
25
|
+
status: str # ok | failed | aborted
|
|
26
|
+
output: Any = None
|
|
27
|
+
error: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ReviewTimeline:
|
|
32
|
+
"""完整审阅时间线:firings 顺序,同 (tick, node) 去重 keep-first。"""
|
|
33
|
+
|
|
34
|
+
module_id: str
|
|
35
|
+
entries: list[ReviewEntry] = field(default_factory=list)
|
|
36
|
+
latest_tick: int | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_db_path(module_id: str, base_dir: Path | None) -> Path:
|
|
40
|
+
"""``<base>/.specmodule/runs/<module_id>/run.sqlite``(与 Module 对齐)。"""
|
|
41
|
+
base = base_dir if base_dir is not None else Path.cwd()
|
|
42
|
+
return base / ".specmodule" / "runs" / module_id / "run.sqlite"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_timeline(module_id: str, base_dir: Path | None = None) -> ReviewTimeline | None:
|
|
46
|
+
"""从 run.sqlite 构建审阅时间线。无 DB / 读失败 → None。"""
|
|
47
|
+
db_path = _run_db_path(module_id, base_dir)
|
|
48
|
+
if not db_path.exists():
|
|
49
|
+
return None
|
|
50
|
+
try:
|
|
51
|
+
from tickflow.persistence import SqliteBackend
|
|
52
|
+
|
|
53
|
+
backend = SqliteBackend(db_path)
|
|
54
|
+
try:
|
|
55
|
+
rows = backend.list_firings(module_id)
|
|
56
|
+
finally:
|
|
57
|
+
backend.close()
|
|
58
|
+
except Exception:
|
|
59
|
+
log.exception("读取 run.sqlite 失败(返回 None): %s", db_path)
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
entries: list[ReviewEntry] = []
|
|
63
|
+
seen: set[tuple[int, str]] = set()
|
|
64
|
+
latest: int | None = None
|
|
65
|
+
for d in rows:
|
|
66
|
+
node = d.get("node")
|
|
67
|
+
if not node:
|
|
68
|
+
continue # 空 node 行跳过(损坏/异常行不阻断时间线)
|
|
69
|
+
try:
|
|
70
|
+
tick = int(d.get("tick", 0))
|
|
71
|
+
except (TypeError, ValueError):
|
|
72
|
+
log.warning("跳过损坏的 firing 行(tick 非法): %r", d)
|
|
73
|
+
continue
|
|
74
|
+
if (tick, node) in seen:
|
|
75
|
+
continue # 与 tickflow audit() 去重语义一致(restore 重放兼容)
|
|
76
|
+
seen.add((tick, node))
|
|
77
|
+
entries.append(
|
|
78
|
+
ReviewEntry(
|
|
79
|
+
tick=tick,
|
|
80
|
+
node=node,
|
|
81
|
+
status=str(d.get("status", "ok")),
|
|
82
|
+
output=d.get("output"),
|
|
83
|
+
error=d.get("error"),
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
latest = tick if latest is None else max(latest, tick)
|
|
87
|
+
return ReviewTimeline(module_id=module_id, entries=entries, latest_tick=latest)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _latest_of(entries: list[ReviewEntry]) -> int | None:
|
|
91
|
+
"""过滤后子集中最新 tick(无条目 → None)。"""
|
|
92
|
+
return max((e.tick for e in entries), default=None)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def filter_failed(timeline: ReviewTimeline) -> ReviewTimeline:
|
|
96
|
+
"""只看失败/中止节点(定位问题 tick 的核心路径)。
|
|
97
|
+
|
|
98
|
+
latest_tick 重算为过滤子集中的最新 tick(避免过滤后仍报全局 tick 误导)。
|
|
99
|
+
"""
|
|
100
|
+
entries = [e for e in timeline.entries if e.status != "ok"]
|
|
101
|
+
return ReviewTimeline(
|
|
102
|
+
module_id=timeline.module_id,
|
|
103
|
+
entries=entries,
|
|
104
|
+
latest_tick=_latest_of(entries),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def filter_tick(timeline: ReviewTimeline, tick: int) -> ReviewTimeline:
|
|
109
|
+
"""只看指定 tick。"""
|
|
110
|
+
entries = [e for e in timeline.entries if e.tick == tick]
|
|
111
|
+
return ReviewTimeline(
|
|
112
|
+
module_id=timeline.module_id,
|
|
113
|
+
entries=entries,
|
|
114
|
+
latest_tick=_latest_of(entries),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def filter_node(timeline: ReviewTimeline, node: str) -> ReviewTimeline:
|
|
119
|
+
"""只看指定节点的全部 firing(含 loop 多轮)。"""
|
|
120
|
+
entries = [e for e in timeline.entries if e.node == node]
|
|
121
|
+
return ReviewTimeline(
|
|
122
|
+
module_id=timeline.module_id,
|
|
123
|
+
entries=entries,
|
|
124
|
+
latest_tick=_latest_of(entries),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class CheckpointEntry:
|
|
130
|
+
"""单个回退点(tick 快照或 manual 检查点)。"""
|
|
131
|
+
|
|
132
|
+
target: str # resume/rollback 直传的目标("3" 或 "manual:xxx")
|
|
133
|
+
tick: int
|
|
134
|
+
kind: str # "tick"(snapshots 表每 tick 快照)| "manual"(checkpoint() 命名点)
|
|
135
|
+
fired: list[str] = field(default_factory=list) # tick 快照本 tick fire 的节点;manual 为空
|
|
136
|
+
label: str | None = None # manual label;tick 快照为 None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class CheckpointList:
|
|
141
|
+
"""全部回退点,按 tick 升序(同 tick 的 manual 排 tick 快照后)。"""
|
|
142
|
+
|
|
143
|
+
module_id: str
|
|
144
|
+
entries: list[CheckpointEntry] = field(default_factory=list)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def build_checkpoints(module_id: str, base_dir: Path | None = None) -> CheckpointList | None:
|
|
148
|
+
"""列出可用回退点(snapshots 表 tick 快照 + checkpoints 表 manual 检查点)。
|
|
149
|
+
|
|
150
|
+
数据源:run.sqlite;无 DB / 读失败 → None(容错哲学同 build_timeline——
|
|
151
|
+
监控方绝不被 DB 锁搞崩)。`resume <target>` / `rollback <target>` 的
|
|
152
|
+
target 即条目 ``target`` 字段。
|
|
153
|
+
"""
|
|
154
|
+
db_path = _run_db_path(module_id, base_dir)
|
|
155
|
+
if not db_path.exists():
|
|
156
|
+
return None
|
|
157
|
+
try:
|
|
158
|
+
from tickflow.persistence import SqliteBackend
|
|
159
|
+
|
|
160
|
+
backend = SqliteBackend(db_path)
|
|
161
|
+
try:
|
|
162
|
+
entries: list[CheckpointEntry] = []
|
|
163
|
+
for tick in backend.list_snapshots(module_id):
|
|
164
|
+
snap = backend.load_snapshot(module_id, tick)
|
|
165
|
+
fired = list(snap.get("fired", [])) if snap else []
|
|
166
|
+
entries.append(
|
|
167
|
+
CheckpointEntry(
|
|
168
|
+
target=str(tick), tick=tick, kind="tick", fired=fired
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
entries.extend(
|
|
172
|
+
CheckpointEntry(target=label, tick=tick, kind="manual", label=label)
|
|
173
|
+
for label, tick in backend.list_checkpoints(module_id)
|
|
174
|
+
)
|
|
175
|
+
finally:
|
|
176
|
+
backend.close()
|
|
177
|
+
except Exception:
|
|
178
|
+
log.exception("读取 run.sqlite 失败(返回 None): %s", db_path)
|
|
179
|
+
return None
|
|
180
|
+
entries.sort(key=lambda e: (e.tick, 0 if e.kind == "tick" else 1))
|
|
181
|
+
return CheckpointList(module_id=module_id, entries=entries)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def checkpoints_to_dict(cl: CheckpointList) -> dict[str, Any]:
|
|
185
|
+
"""JSON 出口(MCP/Web 直接消费同一函数)。"""
|
|
186
|
+
return {
|
|
187
|
+
"module_id": cl.module_id,
|
|
188
|
+
"checkpoints": [
|
|
189
|
+
{
|
|
190
|
+
"target": e.target,
|
|
191
|
+
"tick": e.tick,
|
|
192
|
+
"kind": e.kind,
|
|
193
|
+
"fired": list(e.fired),
|
|
194
|
+
"label": e.label,
|
|
195
|
+
}
|
|
196
|
+
for e in cl.entries
|
|
197
|
+
],
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def timeline_to_dict(timeline: ReviewTimeline) -> dict[str, Any]:
|
|
202
|
+
"""JSON 出口(MCP/Web 直接消费同一函数)。"""
|
|
203
|
+
return {
|
|
204
|
+
"module_id": timeline.module_id,
|
|
205
|
+
"latest_tick": timeline.latest_tick,
|
|
206
|
+
"entries": [
|
|
207
|
+
{
|
|
208
|
+
"tick": e.tick,
|
|
209
|
+
"node": e.node,
|
|
210
|
+
"status": e.status,
|
|
211
|
+
"output": e.output,
|
|
212
|
+
"error": e.error,
|
|
213
|
+
}
|
|
214
|
+
for e in timeline.entries
|
|
215
|
+
],
|
|
216
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# module_harness/registry.py
|
|
2
|
+
"""HarnessRegistry — tickflow Registry 子类,管理 harness/script 注册。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
from tickflow import Registry
|
|
12
|
+
from tickflow.views import DictView
|
|
13
|
+
|
|
14
|
+
from .config import HarnessConfig
|
|
15
|
+
from .command import Command, CommandConfig
|
|
16
|
+
from .harness import Harness
|
|
17
|
+
from .events import (
|
|
18
|
+
EventBus,
|
|
19
|
+
ScriptStarted,
|
|
20
|
+
ScriptCompleted,
|
|
21
|
+
ScriptFailed,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HarnessRegistry(Registry):
|
|
26
|
+
"""tickflow Registry 子类。tickflow 零修改。
|
|
27
|
+
|
|
28
|
+
Runner 只调 ``get_body()``,不感知 harness/script/body 的区别。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
llm_client: Any,
|
|
35
|
+
event_bus: EventBus | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__()
|
|
38
|
+
self._llm_client = llm_client
|
|
39
|
+
self._event_bus = event_bus or EventBus.null()
|
|
40
|
+
self._harness_cfgs: dict[str, HarnessConfig] = {}
|
|
41
|
+
self._script_names: set[str] = set()
|
|
42
|
+
self._command_cfgs: dict[str, CommandConfig] = {}
|
|
43
|
+
|
|
44
|
+
# ── harness 注册 ──────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
def harness(
|
|
47
|
+
self,
|
|
48
|
+
name: str,
|
|
49
|
+
config: HarnessConfig,
|
|
50
|
+
*,
|
|
51
|
+
promptmode: str | None = None,
|
|
52
|
+
prompt_extra: str | None = None,
|
|
53
|
+
spec_inputs: dict[str, Any] | None = None,
|
|
54
|
+
input_aliases: dict[str, str] | None = None,
|
|
55
|
+
) -> "HarnessRegistry":
|
|
56
|
+
"""注册一个 harness body。
|
|
57
|
+
|
|
58
|
+
``name`` 是 graph 中 ``node.body`` 引用的名称。
|
|
59
|
+
``spec_inputs``:spec 字段常量,渲染时作为占位符兜底值。
|
|
60
|
+
``input_aliases``:跨节点输入别名 {field_name: producer},
|
|
61
|
+
prompt 的 {field} 占位符在运行时解析 producer 的输出值。
|
|
62
|
+
返回 self,支持链式调用。
|
|
63
|
+
"""
|
|
64
|
+
h = Harness(config, self._llm_client, self._event_bus)
|
|
65
|
+
body = h.build_body(
|
|
66
|
+
promptmode=promptmode,
|
|
67
|
+
prompt_extra=prompt_extra,
|
|
68
|
+
spec_inputs=spec_inputs,
|
|
69
|
+
input_aliases=input_aliases,
|
|
70
|
+
)
|
|
71
|
+
self.body(name, body)
|
|
72
|
+
self._harness_cfgs[name] = config
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
# ── script 注册 ───────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
def script(self, name: str):
|
|
78
|
+
"""装饰器:``@reg.script('name')`` — 包裹事件发射后注册为 body。
|
|
79
|
+
|
|
80
|
+
body 执行时自动发射 ScriptStarted / ScriptCompleted / ScriptFailed。
|
|
81
|
+
支持 sync 和 async 用户函数。
|
|
82
|
+
"""
|
|
83
|
+
bus = self._event_bus
|
|
84
|
+
|
|
85
|
+
def deco(fn: Callable) -> Callable:
|
|
86
|
+
is_async = inspect.iscoroutinefunction(fn)
|
|
87
|
+
|
|
88
|
+
if is_async:
|
|
89
|
+
@functools.wraps(fn)
|
|
90
|
+
async def wrapped(view: DictView) -> Any:
|
|
91
|
+
node = view.node
|
|
92
|
+
bus.emit(ScriptStarted(
|
|
93
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
94
|
+
))
|
|
95
|
+
try:
|
|
96
|
+
result = await fn(view)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
bus.emit(ScriptFailed(
|
|
99
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
100
|
+
error=str(e),
|
|
101
|
+
))
|
|
102
|
+
raise
|
|
103
|
+
bus.emit(ScriptCompleted(
|
|
104
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
105
|
+
output_type=type(result).__name__,
|
|
106
|
+
))
|
|
107
|
+
return result
|
|
108
|
+
else:
|
|
109
|
+
@functools.wraps(fn)
|
|
110
|
+
def wrapped(view: DictView) -> Any:
|
|
111
|
+
node = view.node
|
|
112
|
+
bus.emit(ScriptStarted(
|
|
113
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
114
|
+
))
|
|
115
|
+
try:
|
|
116
|
+
result = fn(view)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
bus.emit(ScriptFailed(
|
|
119
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
120
|
+
error=str(e),
|
|
121
|
+
))
|
|
122
|
+
raise
|
|
123
|
+
bus.emit(ScriptCompleted(
|
|
124
|
+
timestamp=time.monotonic(), node=node, tick=0,
|
|
125
|
+
output_type=type(result).__name__,
|
|
126
|
+
))
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
self.body(name, wrapped)
|
|
130
|
+
self._script_names.add(name)
|
|
131
|
+
return wrapped
|
|
132
|
+
|
|
133
|
+
return deco
|
|
134
|
+
|
|
135
|
+
# ── command 注册 ───────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
def command(
|
|
138
|
+
self,
|
|
139
|
+
name: str,
|
|
140
|
+
config: CommandConfig,
|
|
141
|
+
*,
|
|
142
|
+
timeout: float | None = None,
|
|
143
|
+
cwd: str | None = None,
|
|
144
|
+
) -> "HarnessRegistry":
|
|
145
|
+
"""注册一个 command body。
|
|
146
|
+
|
|
147
|
+
``name`` 是 graph 中 ``node.body`` 引用的名称。
|
|
148
|
+
返回 self,支持链式调用。
|
|
149
|
+
"""
|
|
150
|
+
cmd = Command(config, self._event_bus)
|
|
151
|
+
body = cmd.build_body(timeout=timeout, cwd=cwd)
|
|
152
|
+
self.body(name, body)
|
|
153
|
+
self._command_cfgs[name] = config
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
# ── 查询 ──────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
def is_harness(self, name: str) -> bool:
|
|
159
|
+
"""name 是否通过 harness() 注册。"""
|
|
160
|
+
return name in self._harness_cfgs
|
|
161
|
+
|
|
162
|
+
def is_script(self, name: str) -> bool:
|
|
163
|
+
"""name 是否通过 script() 注册。"""
|
|
164
|
+
return name in self._script_names
|
|
165
|
+
|
|
166
|
+
def is_command(self, name: str) -> bool:
|
|
167
|
+
"""name 是否通过 command() 注册。"""
|
|
168
|
+
return name in self._command_cfgs
|
|
169
|
+
|
|
170
|
+
def harness_config(self, name: str) -> HarnessConfig | None:
|
|
171
|
+
"""返回 harness 的配置,若不是 harness 返回 None。"""
|
|
172
|
+
return self._harness_cfgs.get(name)
|
|
173
|
+
|
|
174
|
+
def command_config(self, name: str) -> CommandConfig | None:
|
|
175
|
+
"""返回 command 的配置,若不是 command 返回 None。"""
|
|
176
|
+
return self._command_cfgs.get(name)
|
|
177
|
+
|
|
178
|
+
def guard_names(self) -> list[str]:
|
|
179
|
+
"""已注册 guard 名列表(publish 单文件转化枚举用)。"""
|
|
180
|
+
return list(getattr(self, "_guards", {}).keys())
|