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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""翻译器:TasklistValidator + TemplateLoader + Translator。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from tickflow import Failure, parse as parse_graph
|
|
14
|
+
from tickflow.views import DictView, Resolved
|
|
15
|
+
|
|
16
|
+
from .config import HarnessConfig
|
|
17
|
+
from .harness import Harness
|
|
18
|
+
from .spec import Spec, Tasklist, TasklistTemplate, TaskDefinition
|
|
19
|
+
from .registry import HarnessRegistry
|
|
20
|
+
|
|
21
|
+
# Regex to find the first node name in a flow line.
|
|
22
|
+
_FIRST_NODE = re.compile(r"^[ \t]*\[?(?P<name>[A-Za-z_][A-Za-z0-9_]*)")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextlib.contextmanager
|
|
26
|
+
def _suppress_parse_noise():
|
|
27
|
+
"""构建/校验期暂时抑制 ``tickflow.parser`` 的结构性假阳性 warning。
|
|
28
|
+
|
|
29
|
+
ModuleHarness 在 ``parse`` **之后**才附加节点 body(isolated 名含 ``:``,
|
|
30
|
+
DSL body 声明无法表达),故 parse 期所有 producer 被判为 body-less,触发
|
|
31
|
+
``node X reads input from Y which has no body`` 假阳性(submodule 漏斗
|
|
32
|
+
会放大到每条输入边)。真实结构错误仍以 ``ParseError`` 抛出,不受影响。
|
|
33
|
+
作用域限定在调用方,退出即恢复原日志级别。
|
|
34
|
+
"""
|
|
35
|
+
parser_log = logging.getLogger("tickflow.parser")
|
|
36
|
+
prev = parser_log.level
|
|
37
|
+
parser_log.setLevel(logging.ERROR)
|
|
38
|
+
try:
|
|
39
|
+
yield
|
|
40
|
+
finally:
|
|
41
|
+
parser_log.setLevel(prev)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def prepare_flow(flow: str) -> str:
|
|
45
|
+
"""Ensure the flow text is valid tickflow DSL.
|
|
46
|
+
|
|
47
|
+
The tasklist ``flow`` field uses a tickflow-like syntax but allows
|
|
48
|
+
bare task names as the whole flow (meaning that task is both the
|
|
49
|
+
start and the only node). This function:
|
|
50
|
+
* wraps a bare name as ``[name]``, and
|
|
51
|
+
* wraps the first token in ``[...]`` when no start marker exists
|
|
52
|
+
(only one start is added for the simplest case).
|
|
53
|
+
"""
|
|
54
|
+
stripped = flow.strip()
|
|
55
|
+
|
|
56
|
+
# Single bare name -> start node.
|
|
57
|
+
if "--> " not in stripped and "-->[" not in stripped and stripped.startswith("[") is False:
|
|
58
|
+
if _FIRST_NODE.fullmatch(stripped):
|
|
59
|
+
return f"[{stripped}]"
|
|
60
|
+
|
|
61
|
+
# Multi-node flow with no ``[start]`` marker at all.
|
|
62
|
+
if "[" not in stripped:
|
|
63
|
+
m = _FIRST_NODE.match(stripped)
|
|
64
|
+
if m:
|
|
65
|
+
first = m.group("name")
|
|
66
|
+
idx = m.end("name")
|
|
67
|
+
return f"[{first}]{stripped[idx:]}"
|
|
68
|
+
|
|
69
|
+
return stripped
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TasklistValidator:
|
|
73
|
+
"""校验 tasklist 的结构合法性与引用完整性。"""
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def validate(
|
|
77
|
+
tasklist: Tasklist,
|
|
78
|
+
registry: HarnessRegistry,
|
|
79
|
+
modules: dict[str, Any] | None = None,
|
|
80
|
+
) -> list[str]:
|
|
81
|
+
"""返回问题列表,空列表 = 合法。``modules`` 为 submodule 名解析表
|
|
82
|
+
(None = 跳过名字校验,向后兼容)。"""
|
|
83
|
+
errors: list[str] = []
|
|
84
|
+
|
|
85
|
+
for key, task in tasklist.tasks.items():
|
|
86
|
+
errors.extend(TasklistValidator._check_task(key, task, registry, modules))
|
|
87
|
+
|
|
88
|
+
errors.extend(TasklistValidator._check_flow(tasklist, registry))
|
|
89
|
+
return errors
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _check_task(
|
|
93
|
+
key: str,
|
|
94
|
+
task: TaskDefinition,
|
|
95
|
+
registry: HarnessRegistry,
|
|
96
|
+
modules: dict[str, Any] | None = None,
|
|
97
|
+
) -> list[str]:
|
|
98
|
+
errors: list[str] = []
|
|
99
|
+
|
|
100
|
+
if task.type == "harness":
|
|
101
|
+
if not task.harness:
|
|
102
|
+
errors.append(f"Task '{key}': type='harness' 但缺少 'harness' 字段")
|
|
103
|
+
elif not registry.is_harness(task.harness) and not registry.has_body(task.harness):
|
|
104
|
+
errors.append(f"Task '{key}': harness '{task.harness}' 未在 registry 中注册")
|
|
105
|
+
elif task.type == "script":
|
|
106
|
+
if not task.script:
|
|
107
|
+
errors.append(f"Task '{key}': type='script' 但缺少 'script' 字段")
|
|
108
|
+
elif not registry.is_script(task.script) and not registry.has_body(task.script):
|
|
109
|
+
errors.append(f"Task '{key}': script '{task.script}' 未在 registry 中注册")
|
|
110
|
+
elif task.type == "command":
|
|
111
|
+
if not task.command:
|
|
112
|
+
errors.append(f"Task '{key}': type='command' 但缺少 'command' 字段")
|
|
113
|
+
elif not registry.is_command(task.command) and not registry.has_body(task.command):
|
|
114
|
+
errors.append(f"Task '{key}': command '{task.command}' 未在 registry 中注册")
|
|
115
|
+
elif task.type == "submodule":
|
|
116
|
+
if not task.submodule:
|
|
117
|
+
errors.append(f"Task '{key}': type='submodule' 但缺少 'submodule' 字段")
|
|
118
|
+
elif modules is not None and task.submodule not in modules:
|
|
119
|
+
errors.append(f"Task '{key}': submodule '{task.submodule}' 未在 modules 中声明")
|
|
120
|
+
else:
|
|
121
|
+
errors.append(f"Task '{key}': 未知 type '{task.type}'")
|
|
122
|
+
|
|
123
|
+
return errors
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _check_flow(tasklist: Tasklist, registry: HarnessRegistry) -> list[str]:
|
|
127
|
+
errors: list[str] = []
|
|
128
|
+
task_keys = set(tasklist.tasks.keys())
|
|
129
|
+
|
|
130
|
+
# 解析 flow 得到 node 名(简单正则提取 mermaid 中的节点)
|
|
131
|
+
# 匹配 A --> B, [A]-->B, A--|g|-->B 等
|
|
132
|
+
node_names: set[str] = set()
|
|
133
|
+
flow = prepare_flow(tasklist.flow)
|
|
134
|
+
# 匹配 start marker [X]
|
|
135
|
+
for m in re.finditer(r'\[(\w+)\]', flow):
|
|
136
|
+
node_names.add(m.group(1))
|
|
137
|
+
# 匹配 X--|...|-->Y 和 X-->Y
|
|
138
|
+
for m in re.finditer(r'(\w+)\s*(?:--\|?\w*\|?)?-->', flow):
|
|
139
|
+
node_names.add(m.group(1))
|
|
140
|
+
for m in re.finditer(r'-->\s*(\w+)', flow):
|
|
141
|
+
node_names.add(m.group(1))
|
|
142
|
+
|
|
143
|
+
# 检查 flow 中出现但不在 tasks 中的节点
|
|
144
|
+
for node in node_names:
|
|
145
|
+
if node not in task_keys:
|
|
146
|
+
errors.append(f"Flow 中引用了未定义的节点 '{node}'")
|
|
147
|
+
|
|
148
|
+
# 检查 tasks 中定义了但不在 flow 中的孤立节点
|
|
149
|
+
for key in task_keys:
|
|
150
|
+
if key not in node_names:
|
|
151
|
+
errors.append(f"Task '{key}' 在 Flow 中未被引用(孤立节点)")
|
|
152
|
+
|
|
153
|
+
# 尝试 tickflow parse 检测语法问题(与 graph_builder 使用相同的 prepare_flow 预处理)
|
|
154
|
+
try:
|
|
155
|
+
with _suppress_parse_noise():
|
|
156
|
+
parse_graph(prepare_flow(flow), registry=registry)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
errors.append(f"Flow 解析失败: {e}")
|
|
159
|
+
|
|
160
|
+
return errors
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TemplateLoader:
|
|
164
|
+
"""加载与管理 tasklist 模板。"""
|
|
165
|
+
|
|
166
|
+
def __init__(self) -> None:
|
|
167
|
+
self._templates: dict[str, TasklistTemplate] = {}
|
|
168
|
+
|
|
169
|
+
def register(self, name: str, data: dict[str, Any]) -> None:
|
|
170
|
+
"""注册一个模板(代码调用或文件加载)。data 直接过 from_json。"""
|
|
171
|
+
self._templates[name] = TasklistTemplate.from_json(data)
|
|
172
|
+
|
|
173
|
+
def get(self, name: str) -> TasklistTemplate | None:
|
|
174
|
+
return self._templates.get(name)
|
|
175
|
+
|
|
176
|
+
def list_names(self) -> list[str]:
|
|
177
|
+
return list(self._templates.keys())
|
|
178
|
+
|
|
179
|
+
def load_directory(self, path: str | Path) -> int:
|
|
180
|
+
"""从目录加载 .json 模板文件。返回加载数量。"""
|
|
181
|
+
p = Path(path)
|
|
182
|
+
count = 0
|
|
183
|
+
if p.is_dir():
|
|
184
|
+
for f in sorted(p.glob("*.json")):
|
|
185
|
+
try:
|
|
186
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
187
|
+
self.register(data["name"], data)
|
|
188
|
+
count += 1
|
|
189
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
190
|
+
pass # 跳过无效文件
|
|
191
|
+
return count
|
|
192
|
+
|
|
193
|
+
def load_builtins(self) -> int:
|
|
194
|
+
"""加载内置模板(module_harness/templates/builtin/)。"""
|
|
195
|
+
builtin_dir = Path(__file__).parent / "templates" / "builtin"
|
|
196
|
+
return self.load_directory(builtin_dir)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Translator:
|
|
200
|
+
"""spec -> tasklist 翻译器。直接调 body/script,不走 tickflow runner。"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, registry: HarnessRegistry) -> None:
|
|
203
|
+
self.reg = registry
|
|
204
|
+
|
|
205
|
+
async def translate(self, spec: Spec, template: TasklistTemplate) -> Tasklist:
|
|
206
|
+
"""执行翻译并返回校验通过的 Tasklist。"""
|
|
207
|
+
ts = template.translation
|
|
208
|
+
|
|
209
|
+
if ts.type == "script":
|
|
210
|
+
tasks_dict = await self._call_script_translator(ts.script, spec)
|
|
211
|
+
elif ts.type == "harness":
|
|
212
|
+
tasks_dict = await self._call_harness_translator(ts.harness, ts.prompt, spec, prompt_core=ts.prompt_core)
|
|
213
|
+
else:
|
|
214
|
+
raise ValueError(f"不支持的 translation type: {ts.type}")
|
|
215
|
+
|
|
216
|
+
# 检测 LLM 返回的包装格式 {"Tasks": {...}, "Flow": "..."}
|
|
217
|
+
flow = template.tasklist.flow
|
|
218
|
+
if isinstance(tasks_dict, dict) and "Tasks" in tasks_dict:
|
|
219
|
+
flow = tasks_dict.get("Flow", flow)
|
|
220
|
+
tasks_dict = tasks_dict["Tasks"]
|
|
221
|
+
|
|
222
|
+
# 兼容 LLM 将 Tasks 输出为数组:转为 {A: ..., B: ...} 格式
|
|
223
|
+
if isinstance(tasks_dict, list):
|
|
224
|
+
import string
|
|
225
|
+
tasks_dict = {
|
|
226
|
+
string.ascii_uppercase[i]: td
|
|
227
|
+
for i, td in enumerate(tasks_dict)
|
|
228
|
+
}
|
|
229
|
+
# 兼容 LLM 将 Flow 输出为数组:["A","B"] → "A --> B"
|
|
230
|
+
if isinstance(flow, list):
|
|
231
|
+
flow = " --> ".join(str(n) for n in flow)
|
|
232
|
+
# 规范化 Flow 语法:容忍 LLM 常见的格式偏差
|
|
233
|
+
if isinstance(flow, str):
|
|
234
|
+
flow = flow.replace("→", "-->") # unicode → ASCII
|
|
235
|
+
flow = flow.replace("--->", "-->") # 防止已经含 --> 的再替换
|
|
236
|
+
if "-->" not in flow:
|
|
237
|
+
flow = flow.replace("->", "-->")
|
|
238
|
+
|
|
239
|
+
# 构建 tasklist 并校验
|
|
240
|
+
tasklist = Tasklist(tasks={
|
|
241
|
+
key: TaskDefinition.from_dict(td) if isinstance(td, dict) else td
|
|
242
|
+
for key, td in tasks_dict.items()
|
|
243
|
+
}, flow=flow)
|
|
244
|
+
|
|
245
|
+
errors = TasklistValidator.validate(tasklist, self.reg)
|
|
246
|
+
if errors:
|
|
247
|
+
raise ValueError(f"翻译结果校验失败:\n" + "\n".join(f" - {e}" for e in errors))
|
|
248
|
+
|
|
249
|
+
return tasklist
|
|
250
|
+
|
|
251
|
+
async def _call_script_translator(self, script_name: str, spec: Spec) -> dict:
|
|
252
|
+
"""直接调用已注册的 script 函数。"""
|
|
253
|
+
body = self.reg.get_body(script_name)
|
|
254
|
+
view = DictView(
|
|
255
|
+
{"spec": Resolved(value=spec.to_dict(), k=None)},
|
|
256
|
+
node="__translator__",
|
|
257
|
+
)
|
|
258
|
+
result = body(view)
|
|
259
|
+
if inspect.isawaitable(result):
|
|
260
|
+
result = await result
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
async def _call_harness_translator(
|
|
264
|
+
self,
|
|
265
|
+
harness_name: str,
|
|
266
|
+
prompt_extra: str,
|
|
267
|
+
spec: Spec,
|
|
268
|
+
prompt_core: str | None = None,
|
|
269
|
+
) -> dict:
|
|
270
|
+
"""调用 harness body(异步 LLM),parse 返回的 JSON 为 task dict。
|
|
271
|
+
|
|
272
|
+
如果提供 *prompt_core*,则覆盖原 harness 配置的核心提示词模板。
|
|
273
|
+
"""
|
|
274
|
+
if prompt_core is not None:
|
|
275
|
+
existing = self.reg.harness_config(harness_name)
|
|
276
|
+
if existing is not None:
|
|
277
|
+
overridden = HarnessConfig(
|
|
278
|
+
prompt_core=prompt_core,
|
|
279
|
+
prompt_modes=dict(existing.prompt_modes),
|
|
280
|
+
output_format=existing.output_format,
|
|
281
|
+
notdo=list(existing.notdo),
|
|
282
|
+
model=existing.model,
|
|
283
|
+
temperature=existing.temperature,
|
|
284
|
+
think=existing.think,
|
|
285
|
+
api_params=dict(existing.api_params),
|
|
286
|
+
)
|
|
287
|
+
h = Harness(overridden, self.reg._llm_client, self.reg._event_bus)
|
|
288
|
+
body = h.build_body()
|
|
289
|
+
else:
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Translation harness '{harness_name}' not found in registry"
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
body = self.reg.get_body(harness_name)
|
|
295
|
+
|
|
296
|
+
view_data: dict[str, Resolved] = {"spec": Resolved(value=spec.to_dict(), k=None)}
|
|
297
|
+
if prompt_extra:
|
|
298
|
+
view_data["prompt_extra"] = Resolved(value=prompt_extra, k=None)
|
|
299
|
+
view = DictView(view_data, node="__translator__")
|
|
300
|
+
result = await body(view)
|
|
301
|
+
|
|
302
|
+
if isinstance(result, Failure):
|
|
303
|
+
raise ValueError(f"翻译 harness 返回 Failure: {result.error}")
|
|
304
|
+
|
|
305
|
+
if isinstance(result, str):
|
|
306
|
+
try:
|
|
307
|
+
return json.loads(result)
|
|
308
|
+
except json.JSONDecodeError as e:
|
|
309
|
+
raise ValueError(f"翻译结果不是合法 JSON: {e}") from e
|
|
310
|
+
|
|
311
|
+
if isinstance(result, dict):
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
raise ValueError(f"翻译结果类型异常: {type(result).__name__}")
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: specmodule
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 可审计、可调试、可完全掌控的 LLM 使用框架(tickflow + llm + module_harness)
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: llm,workflow,petri-net,agent
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: tickflow-py>=0.1.0
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: anthropic; extra == "anthropic"
|
|
21
|
+
Provides-Extra: openai
|
|
22
|
+
Requires-Dist: openai; extra == "openai"
|
|
23
|
+
Provides-Extra: validate
|
|
24
|
+
Requires-Dist: jsonschema; extra == "validate"
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# SpecModule
|
|
30
|
+
|
|
31
|
+
可审计、可调试、可完全掌控的 LLM 使用框架。
|
|
32
|
+
|
|
33
|
+
将 LLM 调用拆分为可组合的 Petri 网节点,每个节点是最小执行单元——翻译、审查、shell 命令、Python 函数。节点通过有向边连接(支持 AND/OR 汇合、循环),引擎以同步步进执行,所有状态集中记录。**每 tick 落盘轻量快照**,快照、暂停、精确回退(tick 号)都是低开销的。
|
|
34
|
+
|
|
35
|
+
## 架构
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
SpecModule/
|
|
39
|
+
├── tickflow # Petri 网工作流引擎(外部 pip 依赖 tickflow-py,import 名 tickflow)
|
|
40
|
+
├── llm/ # LLM 客户端(Anthropic + OpenAI 兼容)
|
|
41
|
+
│ ├── client.py
|
|
42
|
+
│ └── config.py
|
|
43
|
+
├── module_harness/ # Module 上层抽象
|
|
44
|
+
│ ├── module.py # Module 编排器(run/resume/snapshot/rollback)
|
|
45
|
+
│ ├── registry.py # HarnessRegistry(harness / script / command 注册)
|
|
46
|
+
│ ├── harness.py # Harness 类(LLM 调用节点,三层 prompt)
|
|
47
|
+
│ ├── command.py # Command 节点(shell 子进程)
|
|
48
|
+
│ ├── prompt.py # 三层 prompt 渲染
|
|
49
|
+
│ ├── outputfmt.py # 输出格式校验 + 自动提取
|
|
50
|
+
│ ├── spec.py # Spec, Tasklist, TasklistTemplate 数据模型
|
|
51
|
+
│ ├── translator.py # spec → tasklist 翻译 + 校验 + 模板加载
|
|
52
|
+
│ ├── graph_builder.py # tasklist → tickflow Graph
|
|
53
|
+
│ ├── consistency.py # spec + tasklist 一致性审核
|
|
54
|
+
│ ├── align.py # 对齐检查 harness
|
|
55
|
+
│ ├── checkpoint.py # 运行输入存档 + resume 兼容性校验
|
|
56
|
+
│ ├── status.py # 跨进程运行状态查询
|
|
57
|
+
│ ├── submodule.py # 类式 module 定义 + 打包发布
|
|
58
|
+
│ ├── loader.py # module 加载 + 依赖校验
|
|
59
|
+
│ ├── builtins.py # 内置 harness 集
|
|
60
|
+
│ ├── events.py # EventBus + 类型化事件
|
|
61
|
+
│ ├── entry.py # ModuleEntry 入口合约 + 目录发现
|
|
62
|
+
│ ├── scaffold.py # init 脚手架生成(单文件 + --as-dir 目录形态)
|
|
63
|
+
│ ├── store.py # store 共享层(家目录/搜索路径/枚举/安装管理)
|
|
64
|
+
│ ├── feed.py # 零依赖运行 feed(http.server,CLI feed 命令)
|
|
65
|
+
│ ├── query.py # 共享查询层(时间线/检查点,CLI/MCP/Web 复用)
|
|
66
|
+
│ ├── cli.py # specmodule CLI(18 子命令,argparse 零依赖)
|
|
67
|
+
│ ├── templates/ # 内置任务模板
|
|
68
|
+
│ └── tests/ # pytest 测试套件(含真实 LLM smoke)
|
|
69
|
+
└── docs/ # 设计文档、实现计划、路线图
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 安装依赖
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# 库:pip 安装(pyproject.toml + console script `specmodule`)
|
|
76
|
+
pip install specmodule
|
|
77
|
+
|
|
78
|
+
# 开发(本仓库):源码 + 测试依赖
|
|
79
|
+
pip install -r requirements.txt
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| 包 | 用途 | 必需 |
|
|
83
|
+
|----|------|------|
|
|
84
|
+
| **`specmodule`** | 库本体(PyPI 名;`pyproject.toml` 打包:`llm` + `module_harness` + CLI `specmodule`) | ✅ 必需 |
|
|
85
|
+
| **`tickflow-py`** | Petri 网工作流引擎。⚠️ PyPI 包名为 `tickflow-py`,**import 名仍为 `tickflow`**(`import tickflow`,不是 `import tickflow_py`)。上游仓库:https://github.com/MountLynx/tickflow- | ✅ 必需 |
|
|
86
|
+
| `anthropic` | Claude 后端(`provider=anthropic` 时) | 按 provider 选装 |
|
|
87
|
+
| `openai` | OpenAI 及兼容后端(`provider=openai` / `openai-compatible` 时) | 按 provider 选装 |
|
|
88
|
+
| `jsonschema` | `json_schema` 输出格式校验(未安装则跳过 schema 校验,仅保证是 JSON) | 推荐 |
|
|
89
|
+
| `pytest` | 测试套件(`python -m pytest module_harness/tests/ -q`) | 仅开发 |
|
|
90
|
+
|
|
91
|
+
安装后 CLI 即用(`specmodule run/status/review/...`,18 个子命令,见 `docs/cli-usage.md`);不写 module 的使用者走 store 闭环:`specmodule setup` 配 key → `install` 装模块 → `list`/`run` → `update`/`uninstall`。
|
|
92
|
+
|
|
93
|
+
## 快速开始
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from module_harness import Module, HarnessRegistry, HarnessConfig, EventBus
|
|
97
|
+
from module_harness import TemplateLoader, OutputFormat
|
|
98
|
+
from llm import create_llm_client, LLMConfig
|
|
99
|
+
|
|
100
|
+
# 1. 准备 LLM 客户端(LLM_PROVIDER / API key 从环境或 .env 读取)
|
|
101
|
+
config = LLMConfig.from_env()
|
|
102
|
+
client = create_llm_client(config)
|
|
103
|
+
bus = EventBus()
|
|
104
|
+
|
|
105
|
+
# 2. 注册 harness 和 script
|
|
106
|
+
reg = HarnessRegistry(llm_client=client, event_bus=bus)
|
|
107
|
+
|
|
108
|
+
reg.harness("translate", HarnessConfig(
|
|
109
|
+
prompt_core="将以下文本翻译为中文:{text}",
|
|
110
|
+
output_format=OutputFormat(type="json_object"),
|
|
111
|
+
notdo=["不要添加解释"],
|
|
112
|
+
temperature=0.3,
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
@reg.script("format_output")
|
|
116
|
+
def format_output(view):
|
|
117
|
+
data = view.A.value
|
|
118
|
+
return {"result": data["translation"].strip()}
|
|
119
|
+
|
|
120
|
+
# 3. 加载内置模板(spec only → 翻译通道)
|
|
121
|
+
loader = TemplateLoader()
|
|
122
|
+
loader.load_builtins()
|
|
123
|
+
|
|
124
|
+
# 4. 运行(persist=True 时每 tick 落盘轻量快照,可精确回退)
|
|
125
|
+
module = Module(
|
|
126
|
+
spec={"source_text": "Hello world", "style": "formal"},
|
|
127
|
+
template_name="translate",
|
|
128
|
+
llm_client=client,
|
|
129
|
+
event_bus=bus,
|
|
130
|
+
template_loader=loader,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
firings = await module.run()
|
|
134
|
+
for f in firings:
|
|
135
|
+
print(f"{f.node}: {f.output}")
|
|
136
|
+
|
|
137
|
+
# 5. 续跑与回退(跨进程)
|
|
138
|
+
await module.resume(rollback_to=3) # 精确回退到 tick 3 后重跑
|
|
139
|
+
module.list_checkpoints() # [(tick, fired 节点列表, kind), ...]
|
|
140
|
+
|
|
141
|
+
# 6. 封装打包:类式定义 → pack 发布 → 加载运行
|
|
142
|
+
from module_harness import SubModule, SpecSchema, TaskDefinition, ModuleLoader, script
|
|
143
|
+
|
|
144
|
+
class Translator(SubModule):
|
|
145
|
+
"""带风格选择的翻译 module(类式定义 + spec_schema 输入契约)。"""
|
|
146
|
+
name = "my_translator"
|
|
147
|
+
version = "1.0.0"
|
|
148
|
+
description = "带风格选择的翻译 module"
|
|
149
|
+
spec_schema = SpecSchema(
|
|
150
|
+
input={"source_text": "str", "style": "str"},
|
|
151
|
+
output={"translation": "str"},
|
|
152
|
+
)
|
|
153
|
+
harnesses = [HarnessConfig(
|
|
154
|
+
name="translate",
|
|
155
|
+
prompt_core="翻译:{text}",
|
|
156
|
+
prompt_modes={"formal": "正式", "casual": "随意"},
|
|
157
|
+
output_format=OutputFormat(type="json_object"),
|
|
158
|
+
)]
|
|
159
|
+
tasklist = Tasklist(
|
|
160
|
+
tasks={
|
|
161
|
+
"A": TaskDefinition(
|
|
162
|
+
type="harness", harness="translate",
|
|
163
|
+
promptmode="{spec.style}", # spec 字段驱动 promptmode
|
|
164
|
+
inputs={"text": "{spec.source_text}"},
|
|
165
|
+
outputformat={"type": "json_object"},
|
|
166
|
+
),
|
|
167
|
+
"B": TaskDefinition(
|
|
168
|
+
type="script", script="format_output", inputs={"data": "A"},
|
|
169
|
+
),
|
|
170
|
+
},
|
|
171
|
+
flow="A --> B",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@script("format_output")
|
|
175
|
+
def format_output(view):
|
|
176
|
+
return {"translation": view.A.value["translation"].strip()}
|
|
177
|
+
|
|
178
|
+
# 直接运行(spec 经 spec_schema 契约校验)
|
|
179
|
+
await Translator(llm_client=client).run({"source_text": "Hello", "style": "formal"})
|
|
180
|
+
|
|
181
|
+
# 打包发布:导出 module.json + harnesses/ + scripts/ + commands/
|
|
182
|
+
dist = Translator().pack("dist/my_translator")
|
|
183
|
+
|
|
184
|
+
# 另一进程/项目加载运行(requires 依赖校验,无需重新定义)
|
|
185
|
+
loaded = ModuleLoader().load(dist)
|
|
186
|
+
await loaded.run({"source_text": "Hello", "style": "casual"})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## 嵌入式使用(宿主项目 import 库)
|
|
190
|
+
|
|
191
|
+
`pip install specmodule` 后另建项目直接 `import` 库面编程 API,把 SpecModule
|
|
192
|
+
当 LLM 工具套件嵌入自己的服务/IDE 插件/Web 后端。最小 demo 见
|
|
193
|
+
[`examples/embed_minimal/`](examples/embed_minimal/main.py)(包含 `--mock`
|
|
194
|
+
免 key 冒烟,可直接运行验证):
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pip install specmodule
|
|
198
|
+
cd examples/embed_minimal && python main.py --mock
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from module_harness import (
|
|
203
|
+
EventBus, HarnessConfig, HarnessRegistry, Module,
|
|
204
|
+
TemplateLoader, OutputFormat, register_builtin_harnesses,
|
|
205
|
+
)
|
|
206
|
+
from llm import LLMConfig, create_llm_client
|
|
207
|
+
|
|
208
|
+
client = create_llm_client(LLMConfig.from_env()) # .env / 环境变量
|
|
209
|
+
bus = EventBus()
|
|
210
|
+
|
|
211
|
+
reg = HarnessRegistry(llm_client=client, event_bus=bus)
|
|
212
|
+
register_builtin_harnesses(reg) # spec_to_tasklist 等内置集
|
|
213
|
+
reg.harness("translate", HarnessConfig(
|
|
214
|
+
prompt_core="将以下文本翻译为中文:{text}",
|
|
215
|
+
output_format=OutputFormat(type="json_object"),
|
|
216
|
+
))
|
|
217
|
+
|
|
218
|
+
loader = TemplateLoader(); loader.load_builtins()
|
|
219
|
+
module = Module(
|
|
220
|
+
spec={"source_text": "Hello world", "style": "formal"},
|
|
221
|
+
template_name="translate",
|
|
222
|
+
llm_client=client, event_bus=bus, registry=reg, template_loader=loader,
|
|
223
|
+
persist=False, status_file=False, keep_records=False, # 嵌入方零落盘/零残留
|
|
224
|
+
)
|
|
225
|
+
await module.run()
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
嵌入要点:
|
|
229
|
+
|
|
230
|
+
- **事件与 records 解耦**(`decouple-embed-events`)——宿主传 `event_bus` 即收
|
|
231
|
+
`OutputValidated` / `HarnessFailed` 等事件做反馈,不拖审计与落盘;不传则静默零开销。
|
|
232
|
+
- **零残留可选**——`persist=False` + `status_file=False` + `keep_records=False`
|
|
233
|
+
时嵌入方磁盘上不留任何 `.specmodule/` 产物。
|
|
234
|
+
- **内置 harness 显式注册**——翻译/审核/对齐 harness 不走隐式加载,
|
|
235
|
+
宿主对 `HarnessRegistry` 调 `register_builtin_harnesses(reg)` 注册。
|
|
236
|
+
- 库面 = `module_harness` 顶层导出(`Module / HarnessRegistry / SubModule /
|
|
237
|
+
Translator / query` 共享层……),导入勿触达内部子模块。
|
|
238
|
+
|
|
239
|
+
## 核心概念
|
|
240
|
+
|
|
241
|
+
### 三种节点类型
|
|
242
|
+
|
|
243
|
+
| 类型 | 用途 | 注册方式 |
|
|
244
|
+
|------|------|----------|
|
|
245
|
+
| **harness** | LLM 调用 — 三层 prompt、输出校验、流式 token | `reg.harness("name", config)` |
|
|
246
|
+
| **script** | 纯 Python 函数 — 处理、计算、IO | `@reg.script("name")` |
|
|
247
|
+
| **command** | Shell 命令 — 一行字符串即节点 | `reg.command("name", CommandConfig(...))` |
|
|
248
|
+
|
|
249
|
+
### spec 与 tasklist
|
|
250
|
+
|
|
251
|
+
- **spec** — 结构化键值对,描述"想要什么"。无预定义 schema,字段由模板设计者定义。
|
|
252
|
+
- **tasklist** — `{Tasks: {A: {...}, B: {...}}, Flow: "A --> B"}`。描述"如何做",每个 Task 映射为一个 tickflow 节点。
|
|
253
|
+
- **两种输入**:① 只传 spec(通过模板翻译为 tasklist)② 传 spec + tasklist(一致性审核后直入 graph builder)。
|
|
254
|
+
|
|
255
|
+
### 快照与回滚(roadmap #5)
|
|
256
|
+
|
|
257
|
+
- **每 tick 轻量快照**:persist=True 时由引擎逐 tick 落盘(剥离审计 records,O(节点+边) 恒定大小),任意 tick 可回退
|
|
258
|
+
- **精确 tick 号回退**:`resume(tick)` 跨进程续跑,只重跑未执行部分(已执行节点输出保留);手动检查点 `checkpoint("label")` / `rollback_to("label")` 永久保留
|
|
259
|
+
- **`list_checkpoints()`**:显示 `(tick, fired 节点列表, kind)`——tick ↔ 节点轨迹,历史审阅的雏形
|
|
260
|
+
- **进程内** `snapshot()` / `restore()` 全量快照,任意分支/回退
|
|
261
|
+
|
|
262
|
+
### 运行状态查询(roadmap #7)
|
|
263
|
+
|
|
264
|
+
跨进程查询:`status.json`(阶段机:idle → translating → reviewing → ... → done)+ `run.sqlite` 最新快照(tick 级:status/tick/fireable/fired + 每节点最新输出)。任何进程可查,不依赖 Module 实例。
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
from module_harness import query_run_status
|
|
268
|
+
st = query_run_status("my_module") # ModuleStatus:phase/tick/fired/outputs/node_states
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### submodule — 类式 module + 打包发布
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from module_harness import SubModule, script, SpecSchema
|
|
275
|
+
|
|
276
|
+
class Dig(SubModule):
|
|
277
|
+
name = "dig"
|
|
278
|
+
spec_schema = SpecSchema(input={"url": "str"})
|
|
279
|
+
tasklist = Tasklist(tasks={...}, flow="...")
|
|
280
|
+
|
|
281
|
+
@script("fetch")
|
|
282
|
+
def fetch(view):
|
|
283
|
+
return {"html": ...}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`SubModule` 类式声明(含 `spec_schema` 输入契约)→ `pack()` 导出可发布清单 → `ModuleLoader` 加载(`requires` 依赖校验)。`mode = "fast"` 零落盘运行。
|
|
287
|
+
|
|
288
|
+
### 一致性审核与对齐检查
|
|
289
|
+
|
|
290
|
+
- **一致性审核** — 自定义 tasklist 通道默认经内置审核 harness(`spec_tasklist_review`)做 spec↔tasklist 语义一致性 LLM 审核,不通过抛 `ConsistencyError` 阻塞
|
|
291
|
+
- **对齐检查** — 内置 `align_check` 节点,对比 spec 目标与产出,输出对齐/偏离分析 + 建议
|
|
292
|
+
|
|
293
|
+
### 事件系统
|
|
294
|
+
|
|
295
|
+
EventBus 提供两层事件——流程级(tickflow hooks:`on_fire`、`on_tick_end`)和节点内部事件(EventBus:prompt 渲染、token 流、命令执行、校验结果)。消费者按需订阅。
|
|
296
|
+
|
|
297
|
+
### 命名空间隔离
|
|
298
|
+
|
|
299
|
+
多个 Module 可在同一进程中共存,body 以 `{module_id}:{key}` 前缀隔离注册。
|
|
300
|
+
|
|
301
|
+
## 当前状态
|
|
302
|
+
|
|
303
|
+
**库核心框架能力已完成**(18 项);**库自身主线已完成**(2026-08-22):打包接线
|
|
304
|
+
(`pyproject.toml` + `specmodule` CLI 随库分发)、module-user-store 全系列
|
|
305
|
+
(store 家目录 / 配置回退链 / 统一枚举 run 打通 / CLI 管理面 setup-install-list-info-
|
|
306
|
+
uninstall-publish-update / init 目录形态)、独立线(嵌入式验证 demo + stdlib 可视化
|
|
307
|
+
feed)。待做:M2 实践线(store 真实验收)、收口 API 稳定化、生态项目(TUI/MCP/Web)。
|
|
308
|
+
完整进度与路线图见 [module-roadmap.md](docs/progress/module-roadmap.md)。
|
|
309
|
+
|
|
310
|
+
## 开发原则
|
|
311
|
+
|
|
312
|
+
- **tickflow 零修改(有条件的)** — tickflow 是外部依赖(PyPI 包 `tickflow-py`,import 名 `tickflow`,上游仓库 https://github.com/MountLynx/tickflow-),仓库内无 tickflow 代码。修改前先判断:改动是否有普适性、是否真正有助于优化 tickflow 本身?**没有 → 不碰**(模块层功能一律通过 `Registry` 子类扩展);**有 → 在上游改**,发布新版 `tickflow-py` 并升级安装版本
|
|
313
|
+
- **两级用户定位** — 框架服务两类用户:**开发者用户**(写 module 并发布)与**使用者用户**(只写 spec/tasklist)。边界不硬——开发者也是使用者,使用者也能按需修改。本质是两个使用场景(**开发场景** vs **使用场景**),新功能开发时明确主要为哪个场景服务
|
|
314
|
+
- **完全掌控** — 无隐式行为,promptmode 选错直接 KeyError,框架不兜底
|
|
315
|
+
- **审计即设计** — 所有状态记录在 RunState 中,快照与回滚是内置能力
|
|
316
|
+
- **SDK 先行** — 新功能实现前先设计所需的数据查询接口,消费形态(CLI/agent/Web)只是 SDK 的薄封装
|
|
317
|
+
- **YAGNI** — 每项功能有明确使用场景才加入
|
|
318
|
+
|
|
319
|
+
## 许可证
|
|
320
|
+
|
|
321
|
+
MIT
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
llm/__init__.py,sha256=V6j38-2nexAMMhdEbAOov87mMMM4ZJh7Xc0V2ijUQLA,328
|
|
2
|
+
llm/client.py,sha256=qKU6HkLUZtdi03wXq8vivTuqe7Iiw5x0mHwTvu4F_To,26682
|
|
3
|
+
llm/config.py,sha256=LFMo2kb9i0BspNXxDoRDQWq57Ft7PciTctGvwqqqzIk,7512
|
|
4
|
+
module_harness/__init__.py,sha256=9JrtWfwJK2rIEeH5PY0SBJaEicNAhaC301gwy1qrV-Q,4812
|
|
5
|
+
module_harness/align.py,sha256=YIojzfz3ExpVVp4aUc19KVSrY96TtYU3KISznSs9iBM,1420
|
|
6
|
+
module_harness/builtins.py,sha256=RCmSJtAG8SDGYaRp81iJU8hVn3yiPB1JUbG1NR8lj1w,1042
|
|
7
|
+
module_harness/checkpoint.py,sha256=dTewwsmWeDXPNoksx5YoNdLedvs35L9XyrqRiLVUPiM,15759
|
|
8
|
+
module_harness/cli.py,sha256=PCbtloc26VYeg_d6_uIvzB2HadIFmY1fBgiKEyiiI5c,61024
|
|
9
|
+
module_harness/command.py,sha256=s-YlNFhezzgqmQqI_qQlcz9xyGRirdUW7pZ96Hoc9-U,3771
|
|
10
|
+
module_harness/config.py,sha256=cft9-nlavXUG7OMk6dUKBreMXUlJZD_UxkTO4Ac7ta4,3334
|
|
11
|
+
module_harness/consistency.py,sha256=q0UcZaHeNYBL86EjmGo_ULGFY1FhgE_s8FX-8rjftPQ,4452
|
|
12
|
+
module_harness/entry.py,sha256=EOJFjSY2yler-YLzqFpitdXE0uWL-_4XQ8PZ8DEzipg,2890
|
|
13
|
+
module_harness/events.py,sha256=XMxk_FgL3I28wXjtmXTHdKBYY47-ZcgmN_8kO8NRDhM,3752
|
|
14
|
+
module_harness/feed.py,sha256=szKUVcPMRd4Q2FA87avwtWLz5ktwQbtK2NyQqYdWZ-Q,7484
|
|
15
|
+
module_harness/graph_builder.py,sha256=LSsd4F38YR2WJ5AnegxFt4Ijf0xPX5JLYv-_Em5Rkl8,14437
|
|
16
|
+
module_harness/harness.py,sha256=0MUtKSNDpbpsA90q2N1kxtsDHNluS-ihLcJfpezpQFc,6390
|
|
17
|
+
module_harness/loader.py,sha256=O3h_IrpGCYxL2gfalubw3voCyir8aafBRuKxvr2a_2w,9322
|
|
18
|
+
module_harness/module.py,sha256=Tu7vMF-tQOL4rM9SkwjOZWCeQ_AXAjQyjiETV12d9SA,19543
|
|
19
|
+
module_harness/outputfmt.py,sha256=tIQAPcL-Sy3Dame4yytwHYPWvK3pSRwcJ3GgZRuH_50,4763
|
|
20
|
+
module_harness/prompt.py,sha256=htr83VyU79PFuokMR7GYpznsgZnTlhePWp_couYub34,2646
|
|
21
|
+
module_harness/query.py,sha256=xtWchH-Ky-c_HyaUH0qU4NpaytjIQmuU3Nr48kuxHkM,7423
|
|
22
|
+
module_harness/registry.py,sha256=6URtYG8g4xBkyn8tc4fWN8rKfmHpezoL1FAAP1t1iw4,6578
|
|
23
|
+
module_harness/scaffold.py,sha256=fvr-GxSMnCkmemEVt0Fj1TG_7zrX5KPH2qFa3uzbIZA,13792
|
|
24
|
+
module_harness/spec.py,sha256=D3ORh_yPfptrkL2GKJenawt8KJpxmSIn2ZxctgB5EZE,6625
|
|
25
|
+
module_harness/status.py,sha256=BJYUgJpySjhcIirXRYkmue7Go1hkTmNav_s059MumaE,3938
|
|
26
|
+
module_harness/store.py,sha256=c46hEku1BbXCQi8UKjHDMa_GAcvQMqCSTwOOVWOnqYU,17195
|
|
27
|
+
module_harness/submodule.py,sha256=f2P1aG6zPyO2nDyghAR_tQq2ohlWvvKJpdYUtfTMrGI,11712
|
|
28
|
+
module_harness/translator.py,sha256=5A5fNvBsFc53VhJklR9nUto8ngUYp052NZ25rcZEMGw,12423
|
|
29
|
+
module_harness/templates/builtin/codereview.json,sha256=ObWAyo6UY5HabTrgU4dTqmHgfnBhNpyUhytxe3P595M,2026
|
|
30
|
+
module_harness/templates/builtin/docwrite.json,sha256=krj6DVnAwrgsOzM0b800kFvbgt6KRgw2Rx07o-leBnc,740
|
|
31
|
+
module_harness/templates/builtin/summarize.json,sha256=L_1RyozWygDJajyRuniiB8-15afsjus7SaoyILtyQvM,530
|
|
32
|
+
module_harness/templates/builtin/translate.json,sha256=go1SBXzItpfLs5pSYZkubUiFRaaSxAZV-Qns-_wPEdo,1179
|
|
33
|
+
specmodule-0.1.0.dist-info/licenses/LICENSE,sha256=25hjFkQhPrYdWcqDJ_ojqr7c3v1rLiCbOQbLJdA3Bsk,1066
|
|
34
|
+
specmodule-0.1.0.dist-info/METADATA,sha256=6LfSE7WzH99IL8YgefmHaHN_Bdef_MFZ1cWWHA-KOsQ,15672
|
|
35
|
+
specmodule-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
36
|
+
specmodule-0.1.0.dist-info/entry_points.txt,sha256=K1iJZpDfpZ8nTS_fJ1uLo-dlMaXhjDrv7pxkiqeXtr0,55
|
|
37
|
+
specmodule-0.1.0.dist-info/top_level.txt,sha256=IhlN35BKPvk_H0Aqn2zlEl58RmF8xJl45Pma7ubVf8Y,19
|
|
38
|
+
specmodule-0.1.0.dist-info/RECORD,,
|