pyquipu-application 0.1.0__tar.gz

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.
@@ -0,0 +1,75 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # tmpfiles
7
+ o.md
8
+ a.md
9
+ a
10
+ o
11
+ t
12
+ tmpfile
13
+
14
+ # Scaffoldings
15
+ context_builder.yml
16
+ context_builder.py
17
+ current_prompts
18
+
19
+ # Distribution / Packaging
20
+ .Python
21
+ build/
22
+ develop-eggs/
23
+ dist/
24
+ downloads/
25
+ eggs/
26
+ .eggs/
27
+ lib/
28
+ lib64/
29
+ parts/
30
+ sdist/
31
+ var/
32
+ wheels/
33
+ *.egg-info/
34
+ .installed.cfg
35
+ *.egg
36
+
37
+ # Virtual Environments
38
+ venv/
39
+ env/
40
+ .env
41
+ .venv/
42
+
43
+ # Testing
44
+ .tox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # IDEs
55
+ .idea/
56
+ .vscode/
57
+ *.swp
58
+ *.swo
59
+
60
+ # Project Specific
61
+ *.log
62
+ axon.log
63
+ # Ignore test vaults or scratchpads if created in root
64
+ vault/
65
+ temp/
66
+ scratch/
67
+ # --- Quipu Dev Infra ---
68
+ .envs/
69
+ .uv/
70
+ sandbox/
71
+ *.egg-info/
72
+ __pycache__/
73
+ .pytest_cache/
74
+ .coverage
75
+ htmlcov/
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyquipu-application
3
+ Version: 0.1.0
4
+ Summary: Core application logic for Quipu, orchestrating engine and runtime.
5
+ Author-email: doucx <doucxldh@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: pyquipu-bus~=0.1.0
11
+ Requires-Dist: pyquipu-common~=0.1.0
12
+ Requires-Dist: pyquipu-engine~=0.1.0
13
+ Requires-Dist: pyquipu-interfaces~=0.1.0
14
+ Requires-Dist: pyquipu-runtime~=0.1.0
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pyquipu-application"
7
+ version = "0.1.0"
8
+ authors = [{ name="doucx", email="doucxldh@gmail.com" }]
9
+ description = "Core application logic for Quipu, orchestrating engine and runtime."
10
+ requires-python = ">=3.10"
11
+ license = "Apache-2.0"
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Operating System :: OS Independent",
15
+ ]
16
+ dependencies = [
17
+ "pyquipu-interfaces ~= 0.1.0",
18
+ "pyquipu-engine ~= 0.1.0",
19
+ "pyquipu-runtime ~= 0.1.0",
20
+ "pyquipu-common ~= 0.1.0",
21
+ "pyquipu-bus ~= 0.1.0",
22
+ ]
23
+
24
+ [tool.uv.sources]
25
+ pyquipu-interfaces = { workspace = true }
26
+ pyquipu-engine = { workspace = true }
27
+ pyquipu-runtime = { workspace = true }
28
+ pyquipu-common = { workspace = true }
29
+ pyquipu-bus = { workspace = true }
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/quipu"]
@@ -0,0 +1,3 @@
1
+ # This allows this package to coexist with other distribution packages
2
+ # that contribute to the 'quipu' namespace.
3
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,214 @@
1
+ import logging
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Callable, Dict, List
5
+
6
+ from quipu.acts import register_core_acts
7
+ from quipu.engine.state_machine import Engine
8
+ from quipu.interfaces.exceptions import ExecutionError as CoreExecutionError
9
+ from quipu.interfaces.exceptions import OperationCancelledError
10
+ from quipu.interfaces.result import QuipuResult
11
+ from quipu.runtime.executor import Executor
12
+ from quipu.runtime.parser import detect_best_parser, get_parser
13
+
14
+ from .factory import create_engine
15
+ from .plugin_manager import PluginManager
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def get_available_acts(work_dir: Path) -> Dict[str, str]:
21
+ # A dummy confirmation handler is used as it's not required for listing.
22
+ # Yolo=True ensures no interactive prompts can be triggered.
23
+ executor = Executor(
24
+ root_dir=work_dir,
25
+ yolo=True,
26
+ confirmation_handler=lambda diff, prompt: True,
27
+ )
28
+ register_core_acts(executor)
29
+ PluginManager().load_from_sources(executor, work_dir)
30
+ return executor.get_registered_acts()
31
+
32
+
33
+ # 定义 ConfirmationHandler 类型别名: (diff_lines, prompt) -> bool
34
+ # 注意: Executor 期望如果不确认则抛出异常,或者返回 False (取决于 Executor 实现)。
35
+ # 为了保持与 CLI 行为一致,调用方传入的 handler 应该在用户拒绝时抛出 OperationCancelledError。
36
+ ConfirmationHandler = Callable[[List[str], str], bool]
37
+
38
+
39
+ class QuipuApplication:
40
+ def __init__(self, work_dir: Path, confirmation_handler: ConfirmationHandler, yolo: bool = False):
41
+ self.work_dir = work_dir
42
+ self.confirmation_handler = confirmation_handler
43
+ self.yolo = yolo
44
+ self.engine: Engine = create_engine(work_dir)
45
+ logger.info(f"Operation boundary set to: {self.work_dir}")
46
+
47
+ def _prepare_workspace(self) -> str:
48
+ current_hash = self.engine.git_db.get_tree_hash()
49
+
50
+ # 1. 正常 Clean: current_node 存在且与当前 hash 一致
51
+ is_node_clean = (self.engine.current_node is not None) and (
52
+ self.engine.current_node.output_tree == current_hash
53
+ )
54
+
55
+ # 2. 创世 Clean: 历史为空 且 当前是空树 (即没有任何文件被追踪)
56
+ EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
57
+ is_genesis_clean = (not self.engine.history_graph) and (current_hash == EMPTY_TREE_HASH)
58
+
59
+ is_clean = is_node_clean or is_genesis_clean
60
+
61
+ if not is_clean:
62
+ self.engine.capture_drift(current_hash)
63
+
64
+ if self.engine.current_node:
65
+ return self.engine.current_node.output_tree
66
+ else:
67
+ return current_hash
68
+
69
+ def _setup_executor(self) -> Executor:
70
+ executor = Executor(
71
+ root_dir=self.work_dir,
72
+ yolo=self.yolo,
73
+ confirmation_handler=self.confirmation_handler,
74
+ )
75
+
76
+ # 加载核心 acts
77
+ register_core_acts(executor)
78
+
79
+ # 加载外部插件
80
+ plugin_manager = PluginManager()
81
+ plugin_manager.load_from_sources(executor, self.work_dir)
82
+
83
+ return executor
84
+
85
+ def run(self, content: str, parser_name: str) -> QuipuResult:
86
+ # --- Phase 1 & 2: Perception & Decision (Lazy Capture) ---
87
+ input_tree_hash = self._prepare_workspace()
88
+
89
+ # --- Phase 3: Action (Execution) ---
90
+ # 3.1 Parser
91
+ final_parser_name = parser_name
92
+ if parser_name == "auto":
93
+ final_parser_name = detect_best_parser(content)
94
+ if final_parser_name != "backtick":
95
+ logger.info(f"🔍 自动检测到解析器: {final_parser_name}")
96
+
97
+ parser = get_parser(final_parser_name)
98
+ statements = parser.parse(content)
99
+
100
+ if not statements:
101
+ return QuipuResult(
102
+ success=True, # No failure, just nothing to do
103
+ exit_code=0,
104
+ message="axon.warning.noStatements",
105
+ msg_kwargs={"parser": final_parser_name},
106
+ )
107
+
108
+ # 3.2 Executor Setup
109
+ executor = self._setup_executor()
110
+
111
+ # 3.3 Execute
112
+ executor.execute(statements)
113
+
114
+ # --- Phase 4: Recording (Plan Crystallization) ---
115
+ final_summary = None
116
+ # 优先级 1: 从 Markdown 内容中提取 # 标题
117
+ title_match = re.search(r"^\s*#{1,6}\s+(.*)", content, re.MULTILINE)
118
+ if title_match:
119
+ final_summary = title_match.group(1).strip()
120
+ # 优先级 2: 从第一个 act 指令生成摘要
121
+ elif statements:
122
+ final_summary = executor.summarize_statement(statements[0])
123
+
124
+ output_tree_hash = self.engine.git_db.get_tree_hash()
125
+
126
+ self.engine.create_plan_node(
127
+ input_tree=input_tree_hash,
128
+ output_tree=output_tree_hash,
129
+ plan_content=content,
130
+ summary_override=final_summary,
131
+ )
132
+
133
+ return QuipuResult(success=True, exit_code=0, message="run.success")
134
+
135
+
136
+ def run_quipu(
137
+ content: str,
138
+ work_dir: Path,
139
+ confirmation_handler: ConfirmationHandler,
140
+ parser_name: str = "auto",
141
+ yolo: bool = False,
142
+ ) -> QuipuResult:
143
+ app = None
144
+ try:
145
+ app = QuipuApplication(work_dir=work_dir, confirmation_handler=confirmation_handler, yolo=yolo)
146
+ return app.run(content=content, parser_name=parser_name)
147
+
148
+ except OperationCancelledError as e:
149
+ logger.info(f"🚫 操作已取消: {e}")
150
+ return QuipuResult(
151
+ success=False, exit_code=2, message="run.error.cancelled", msg_kwargs={"error": str(e)}, error=e
152
+ )
153
+
154
+ except CoreExecutionError as e:
155
+ logger.error(f"❌ 操作失败: {e}")
156
+ return QuipuResult(
157
+ success=False, exit_code=1, message="run.error.execution", msg_kwargs={"error": str(e)}, error=e
158
+ )
159
+
160
+ except Exception as e:
161
+ logger.error(f"运行时错误: {e}", exc_info=True)
162
+ return QuipuResult(
163
+ success=False, exit_code=1, message="run.error.system", msg_kwargs={"error": str(e)}, error=e
164
+ )
165
+ finally:
166
+ # 确保无论成功或失败,引擎资源都被关闭
167
+ if app and hasattr(app, "engine") and app.engine:
168
+ app.engine.close()
169
+
170
+
171
+ def run_stateless_plan(
172
+ content: str,
173
+ work_dir: Path,
174
+ confirmation_handler: ConfirmationHandler,
175
+ parser_name: str = "auto",
176
+ yolo: bool = False,
177
+ ) -> QuipuResult:
178
+ try:
179
+ executor = Executor(
180
+ root_dir=work_dir,
181
+ yolo=yolo,
182
+ confirmation_handler=confirmation_handler,
183
+ )
184
+ register_core_acts(executor)
185
+ PluginManager().load_from_sources(executor, work_dir)
186
+
187
+ final_parser_name = parser_name
188
+ if parser_name == "auto":
189
+ final_parser_name = detect_best_parser(content)
190
+
191
+ parser = get_parser(final_parser_name)
192
+ statements = parser.parse(content)
193
+
194
+ if not statements:
195
+ return QuipuResult(
196
+ success=True,
197
+ exit_code=0,
198
+ message="axon.warning.noStatements",
199
+ msg_kwargs={"parser": final_parser_name},
200
+ )
201
+
202
+ executor.execute(statements)
203
+ return QuipuResult(success=True, exit_code=0, message="axon.success")
204
+
205
+ except CoreExecutionError as e:
206
+ logger.error(f"❌ 操作失败: {e}")
207
+ return QuipuResult(
208
+ success=False, exit_code=1, message="run.error.execution", msg_kwargs={"error": str(e)}, error=e
209
+ )
210
+ except Exception as e:
211
+ logger.error(f"运行时错误: {e}", exc_info=True)
212
+ return QuipuResult(
213
+ success=False, exit_code=1, message="run.error.system", msg_kwargs={"error": str(e)}, error=e
214
+ )
@@ -0,0 +1,41 @@
1
+ "QuipuApplication": |-
2
+ 封装了 Quipu 核心业务流程的高层应用对象。
3
+ 负责协调 Engine, Parser, Executor。
4
+ "QuipuApplication._prepare_workspace": |-
5
+ 检查并准备工作区,处理状态漂移。
6
+ 返回执行前的 input_tree_hash。
7
+ "QuipuApplication._setup_executor": |-
8
+ 创建、配置并返回一个 Executor 实例,并注入确认处理器。
9
+ "QuipuApplication.run": |-
10
+ 执行一个完整的 Plan。
11
+ "run_quipu": |-
12
+ Quipu 核心业务逻辑的入口包装器。
13
+
14
+ 实例化并运行 QuipuApplication,捕获所有异常并转化为 QuipuResult。
15
+ 确保资源被安全释放。
16
+ get_available_acts: |-
17
+ Statelessly discovers and returns all available acts.
18
+
19
+ This function initializes a temporary, stateless executor to discover
20
+ core acts and acts from plugins found relative to the working directory.
21
+
22
+ Args:
23
+ work_dir: The directory from which to discover project-level plugins.
24
+
25
+ Returns:
26
+ A dictionary mapping act names to their docstrings.
27
+ run_stateless_plan: |-
28
+ Executes a plan in a stateless manner, bypassing the Quipu engine.
29
+
30
+ This function sets up a temporary executor, loads plugins, parses the content,
31
+ and executes the statements against the specified working directory.
32
+
33
+ Args:
34
+ content: The string content of the plan to execute.
35
+ work_dir: The root directory for the execution.
36
+ confirmation_handler: A callable to handle user confirmations.
37
+ parser_name: The name of the parser to use ('auto' by default).
38
+ yolo: If True, skips all confirmation prompts.
39
+
40
+ Returns:
41
+ A QuipuResult object indicating the outcome of the execution.
@@ -0,0 +1,56 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from quipu.engine.config import ConfigManager
5
+ from quipu.engine.git_db import GitDB
6
+ from quipu.engine.git_object_storage import GitObjectHistoryReader, GitObjectHistoryWriter
7
+ from quipu.engine.state_machine import Engine
8
+
9
+ from .utils import find_git_repository_root
10
+
11
+ # 迟延导入以避免循环依赖
12
+ try:
13
+ from quipu.engine.sqlite_db import DatabaseManager
14
+ from quipu.engine.sqlite_storage import SQLiteHistoryReader, SQLiteHistoryWriter
15
+ except ImportError:
16
+ DatabaseManager = None
17
+ SQLiteHistoryWriter = None
18
+ SQLiteHistoryReader = None
19
+
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def create_engine(work_dir: Path, lazy: bool = False) -> Engine:
25
+ project_root = find_git_repository_root(work_dir) or work_dir
26
+ config = ConfigManager(project_root)
27
+ storage_type = config.get("storage.type", "git_object")
28
+ logger.debug(f"Engine factory configured with storage type: '{storage_type}'")
29
+ git_db = GitDB(project_root)
30
+ db_manager = None
31
+
32
+ # 默认和备用后端
33
+ reader = GitObjectHistoryReader(git_db)
34
+ writer = GitObjectHistoryWriter(git_db)
35
+
36
+ if storage_type == "sqlite":
37
+ if not DatabaseManager or not SQLiteHistoryWriter or not SQLiteHistoryReader:
38
+ raise ImportError("SQLite dependencies could not be loaded. Please check your installation.")
39
+
40
+ logger.debug("Using SQLite storage format for reads and writes.")
41
+ db_manager = DatabaseManager(project_root)
42
+ db_manager.init_schema()
43
+
44
+ # 切换到 SQLite 后端
45
+ reader = SQLiteHistoryReader(db_manager=db_manager, git_db=git_db)
46
+ writer = SQLiteHistoryWriter(git_writer=writer, db_manager=db_manager)
47
+
48
+ elif storage_type != "git_object":
49
+ raise NotImplementedError(f"Storage type '{storage_type}' is not supported.")
50
+
51
+ # 将所有资源注入 Engine
52
+ engine = Engine(project_root, db=git_db, reader=reader, writer=writer, db_manager=db_manager)
53
+ if not lazy:
54
+ engine.align()
55
+
56
+ return engine
@@ -0,0 +1,10 @@
1
+ "create_engine": |-
2
+ 实例化完整的 Engine 堆栈。
3
+
4
+ 会自动向上查找项目根目录 (Git Root) 来初始化 Engine。
5
+ 此工厂由配置驱动,以决定使用何种存储后端。
6
+
7
+ Args:
8
+ work_dir: 操作的工作区目录。
9
+ lazy: 如果为 True,则不立即加载完整的历史图谱 (不调用 align)。
10
+ 这对于需要快速启动并按需加载数据的场景 (如 UI) 至关重要。
@@ -0,0 +1,40 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from quipu.runtime.executor import Executor
5
+ from quipu.runtime.plugin_loader import load_plugins
6
+
7
+ from .utils import find_git_repository_root
8
+
9
+
10
+ class PluginManager:
11
+ def load_from_sources(self, executor: Executor, work_dir: Path):
12
+ plugin_sources = []
13
+
14
+ # 优先级由低到高添加,后面的会覆盖前面的
15
+ # 1. User Home (Lowest priority)
16
+ home_acts = Path.home() / ".quipu" / "acts"
17
+ plugin_sources.append(("🏠 Global", home_acts))
18
+
19
+ # 2. Config / Env
20
+ env_path = os.getenv("AXON_EXTRA_ACTS_DIR")
21
+ if env_path:
22
+ plugin_sources.append(("🔧 Env", Path(env_path)))
23
+
24
+ # 3. Project Root (Highest priority)
25
+ project_root_for_plugins = find_git_repository_root(work_dir)
26
+ if project_root_for_plugins:
27
+ proj_acts = project_root_for_plugins / ".quipu" / "acts"
28
+ plugin_sources.append(("📦 Project", proj_acts))
29
+
30
+ seen_paths = set()
31
+ for label, path in plugin_sources:
32
+ if not path.exists() or not path.is_dir():
33
+ continue
34
+
35
+ resolved_path = path.resolve()
36
+ if resolved_path in seen_paths:
37
+ continue
38
+
39
+ load_plugins(executor, path)
40
+ seen_paths.add(resolved_path)
@@ -0,0 +1,5 @@
1
+ "PluginManager": |-
2
+ 负责发现、加载和注册外部插件。
3
+ "PluginManager.load_from_sources": |-
4
+ 按照层级顺序加载外部插件,高优先级会覆盖低优先级。
5
+ 优先级顺序: Project > Env > Home
@@ -0,0 +1,16 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+
8
+ def find_git_repository_root(start_path: Path) -> Optional[Path]:
9
+ try:
10
+ current = start_path.resolve()
11
+ for parent in [current] + list(current.parents):
12
+ if (parent / ".git").exists():
13
+ return parent
14
+ except Exception:
15
+ pass
16
+ return None
@@ -0,0 +1,2 @@
1
+ "find_git_repository_root": |-
2
+ 向上递归查找包含 .git 的目录作为项目根目录
@@ -0,0 +1,178 @@
1
+ {
2
+ "fingerprints": {
3
+ "py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication": {
4
+ "baseline_code_signature_text": "class QuipuApplication:",
5
+ "baseline_code_structure_hash": "d2fb5198cd42afc8c323a52537de5b33164c9a4bb8afa8c3405447eddbafe483",
6
+ "baseline_yaml_content_hash": "2097b05b0863d0d255508bf524ec72afc5bb74f8a8472a9d5c42db1d78375647"
7
+ },
8
+ "py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication.__init__": {
9
+ "baseline_code_signature_text": "def __init__(self, work_dir: Path, confirmation_handler: ConfirmationHandler, yolo: bool = False):",
10
+ "baseline_code_structure_hash": "b2ac0b22c850eed01f7838721b9a9040bba40f58237545f3f86d306fa4327f22"
11
+ },
12
+ "py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication._prepare_workspace": {
13
+ "baseline_code_signature_text": "def _prepare_workspace(self) -> str:",
14
+ "baseline_code_structure_hash": "528a79ec9be83b31b3016bc3a8c5492b13b106464c5bcdca495b4fae187b574d",
15
+ "baseline_yaml_content_hash": "81dd6b666cf091c621c87fe727fe2f20be9183179073f9c25993d0781ac9ac58"
16
+ },
17
+ "py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication._setup_executor": {
18
+ "baseline_code_signature_text": "def _setup_executor(self) -> Executor:",
19
+ "baseline_code_structure_hash": "2cccb8bc4e91794d693f8f3f1f9c53109587bcdd7f5b158ca72f92234e3d74cd",
20
+ "baseline_yaml_content_hash": "9881f5ff456f048f0f958362c910fedcfb0738b4f461dd8a0e04d6fbc30d0944"
21
+ },
22
+ "py://packages/pyquipu-application/src/quipu/application/controller.py#QuipuApplication.run": {
23
+ "baseline_code_signature_text": "def run(self, content: str, parser_name: str) -> QuipuResult:",
24
+ "baseline_code_structure_hash": "458e8779b9f0df2331eada9d10d5fa4fe729a00ed7a5fde70b771d9beb5bfa8b",
25
+ "baseline_yaml_content_hash": "ba87b5c38065687528f0d842b3c042da0e4c164a6019a46707ead5b7f2488997"
26
+ },
27
+ "py://packages/pyquipu-application/src/quipu/application/controller.py#get_available_acts": {
28
+ "baseline_code_signature_text": "def get_available_acts(work_dir: Path) -> Dict[str, str]:",
29
+ "baseline_code_structure_hash": "442aa8ba3388add2d240e0f9bc612bf35528e11d40c765c6ec3a44e4eb874f8b",
30
+ "baseline_yaml_content_hash": "bd97f637450a7cf01f330f5106a83d9e916dde59d64f2fe89ba667ea8e23e4ac"
31
+ },
32
+ "py://packages/pyquipu-application/src/quipu/application/controller.py#run_quipu": {
33
+ "baseline_code_signature_text": "def run_quipu(content: str, work_dir: Path, confirmation_handler: ConfirmationHandler, parser_name: str = 'auto', yolo: bool = False) -> QuipuResult:",
34
+ "baseline_code_structure_hash": "d977d58a9db1f2c9d7fb2d5fabb9cccc5edcdb1aeff988f1fa78442b229c699c",
35
+ "baseline_yaml_content_hash": "316165fbe76e51b117eb3c9c983c8d479df4490b601609f2b0aa5176c9cbd96b"
36
+ },
37
+ "py://packages/pyquipu-application/src/quipu/application/controller.py#run_stateless_plan": {
38
+ "baseline_code_signature_text": "def run_stateless_plan(content: str, work_dir: Path, confirmation_handler: ConfirmationHandler, parser_name: str = 'auto', yolo: bool = False) -> QuipuResult:",
39
+ "baseline_code_structure_hash": "58320dcc77cb54d7ab31bafcc383b0c8dd3953aaeaf9f8208dab0af5094ffd4e",
40
+ "baseline_yaml_content_hash": "aa58fd5cec8b7c127c3a729915c6d4f99c875e4405edba365a5f03f5dff992f7"
41
+ },
42
+ "py://packages/pyquipu-application/src/quipu/application/factory.py#create_engine": {
43
+ "baseline_code_signature_text": "def create_engine(work_dir: Path, lazy: bool = False) -> Engine:",
44
+ "baseline_code_structure_hash": "4565a1494d7662227444205f34a6e4458c80db2856e465388e6d987a15c015c2",
45
+ "baseline_yaml_content_hash": "3dba786e050a1669e0b95d77fdb347afd214cb596dec260790fbfad6963db840"
46
+ },
47
+ "py://packages/pyquipu-application/src/quipu/application/plugin_manager.py#PluginManager": {
48
+ "baseline_code_signature_text": "class PluginManager:",
49
+ "baseline_code_structure_hash": "8d211ba7f4db76c8fa4afc820902583d5f7df5c7e482f99b988dba61ece4a2f5",
50
+ "baseline_yaml_content_hash": "c22797fdf50ccd3ac64731ca93183e2f70839b0611a049639bdc77b4f8fe7f15"
51
+ },
52
+ "py://packages/pyquipu-application/src/quipu/application/plugin_manager.py#PluginManager.load_from_sources": {
53
+ "baseline_code_signature_text": "def load_from_sources(self, executor: Executor, work_dir: Path):",
54
+ "baseline_code_structure_hash": "e1015eaa8d17516183baa58ec0239c567900a308ec70f60b9ce80fa9e1c72fd4",
55
+ "baseline_yaml_content_hash": "34b1c59e7fa9a03d089fccdafa494a216e5a7746e0aff3b5891a262b88986369"
56
+ },
57
+ "py://packages/pyquipu-application/src/quipu/application/utils.py#find_git_repository_root": {
58
+ "baseline_code_signature_text": "def find_git_repository_root(start_path: Path) -> Optional[Path]:",
59
+ "baseline_code_structure_hash": "9f19014baa92289d9b4f04d4ee8a92df475d31c38820edc4ffc20efc9ce218a8",
60
+ "baseline_yaml_content_hash": "8ab446ed5e94789aede646cc369436537ffdd64fc5da3229937ba99d5bc83bb7"
61
+ },
62
+ "py://packages/pyquipu-application/tests/unit/test_controller.py#TestControllerUnit": {
63
+ "baseline_code_signature_text": "class TestControllerUnit:",
64
+ "baseline_code_structure_hash": "b8668020e45ac212142e3e2194f2ce94369784ba3969a4757515a4059960e650",
65
+ "baseline_yaml_content_hash": "e2a45f750adc799b997d14d1c706d2542d3856ce9aba7773a49c5ba2ab15f1d3"
66
+ },
67
+ "py://packages/pyquipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_empty_plan": {
68
+ "baseline_code_signature_text": "def test_run_quipu_empty_plan(self, tmp_path, mock_engine, mock_runtime):",
69
+ "baseline_code_structure_hash": "d5d7459ace504109a1e35f058714a51eb8119c0ea9051168960d877179082f55",
70
+ "baseline_yaml_content_hash": "1071d0c4058fdf8a5b23344da1028be9c9f008be34eb421479e19174b14c4b6a"
71
+ },
72
+ "py://packages/pyquipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_execution_error": {
73
+ "baseline_code_signature_text": "def test_run_quipu_execution_error(self, tmp_path, mock_engine, mock_runtime):",
74
+ "baseline_code_structure_hash": "97f2e3585c826243e7b61c6b4e3ae485f9986b81c50ef6c72058494a5b12960b",
75
+ "baseline_yaml_content_hash": "b8ffd9eec97c064fabd6851169bed6211c276c164ec09f3747a2203127b28bf4"
76
+ },
77
+ "py://packages/pyquipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_success": {
78
+ "baseline_code_signature_text": "def test_run_quipu_success(self, tmp_path, mock_engine, mock_runtime):",
79
+ "baseline_code_structure_hash": "eed582f286876d423157f91ac81d1e7963b67cf3eaf66750e14526adf7832146",
80
+ "baseline_yaml_content_hash": "51d669c5b333dde1c9c4080b4e32e99ee7bfc81f069c896b5dc234b543787810"
81
+ },
82
+ "py://packages/pyquipu-application/tests/unit/test_utils.py#TestRootDiscovery": {
83
+ "baseline_code_signature_text": "class TestRootDiscovery:",
84
+ "baseline_code_structure_hash": "3b99ed1f64e93ef2a1c0050627ddd0d49734803e0b2e6f16697299dd446a4888"
85
+ },
86
+ "py://packages/pyquipu-application/tests/unit/test_utils.py#TestRootDiscovery.test_find_git_repository_root": {
87
+ "baseline_code_signature_text": "def test_find_git_repository_root(self, tmp_path: Path):",
88
+ "baseline_code_structure_hash": "512797d245dc49e41a5d3c7c638481b2b0bc3efa3c6be7b89b058842beea7641"
89
+ },
90
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#ConfirmationHandler": {},
91
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication": {
92
+ "baseline_code_signature_text": "class QuipuApplication:",
93
+ "baseline_code_structure_hash": "d2fb5198cd42afc8c323a52537de5b33164c9a4bb8afa8c3405447eddbafe483",
94
+ "baseline_yaml_content_hash": "2097b05b0863d0d255508bf524ec72afc5bb74f8a8472a9d5c42db1d78375647"
95
+ },
96
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.__init__": {
97
+ "baseline_code_signature_text": "def __init__(self, work_dir: Path, confirmation_handler: ConfirmationHandler, yolo: bool = False):",
98
+ "baseline_code_structure_hash": "b2ac0b22c850eed01f7838721b9a9040bba40f58237545f3f86d306fa4327f22"
99
+ },
100
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication._prepare_workspace": {
101
+ "baseline_code_signature_text": "def _prepare_workspace(self) -> str:",
102
+ "baseline_code_structure_hash": "528a79ec9be83b31b3016bc3a8c5492b13b106464c5bcdca495b4fae187b574d",
103
+ "baseline_yaml_content_hash": "81dd6b666cf091c621c87fe727fe2f20be9183179073f9c25993d0781ac9ac58"
104
+ },
105
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication._setup_executor": {
106
+ "baseline_code_signature_text": "def _setup_executor(self) -> Executor:",
107
+ "baseline_code_structure_hash": "2cccb8bc4e91794d693f8f3f1f9c53109587bcdd7f5b158ca72f92234e3d74cd",
108
+ "baseline_yaml_content_hash": "9881f5ff456f048f0f958362c910fedcfb0738b4f461dd8a0e04d6fbc30d0944"
109
+ },
110
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.confirmation_handler": {},
111
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.engine": {},
112
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.run": {
113
+ "baseline_code_signature_text": "def run(self, content: str, parser_name: str) -> QuipuResult:",
114
+ "baseline_code_structure_hash": "458e8779b9f0df2331eada9d10d5fa4fe729a00ed7a5fde70b771d9beb5bfa8b",
115
+ "baseline_yaml_content_hash": "ba87b5c38065687528f0d842b3c042da0e4c164a6019a46707ead5b7f2488997"
116
+ },
117
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.work_dir": {},
118
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#QuipuApplication.yolo": {},
119
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#logger": {},
120
+ "py://packages/quipu-application/src/pyquipu/application/controller.py#run_quipu": {
121
+ "baseline_code_signature_text": "def run_quipu(content: str, work_dir: Path, confirmation_handler: ConfirmationHandler, parser_name: str = 'auto', yolo: bool = False) -> QuipuResult:",
122
+ "baseline_code_structure_hash": "d977d58a9db1f2c9d7fb2d5fabb9cccc5edcdb1aeff988f1fa78442b229c699c",
123
+ "baseline_yaml_content_hash": "316165fbe76e51b117eb3c9c983c8d479df4490b601609f2b0aa5176c9cbd96b"
124
+ },
125
+ "py://packages/quipu-application/src/pyquipu/application/factory.py#create_engine": {
126
+ "baseline_code_signature_text": "def create_engine(work_dir: Path, lazy: bool = False) -> Engine:",
127
+ "baseline_code_structure_hash": "4565a1494d7662227444205f34a6e4458c80db2856e465388e6d987a15c015c2",
128
+ "baseline_yaml_content_hash": "3dba786e050a1669e0b95d77fdb347afd214cb596dec260790fbfad6963db840"
129
+ },
130
+ "py://packages/quipu-application/src/pyquipu/application/factory.py#logger": {},
131
+ "py://packages/quipu-application/src/pyquipu/application/plugin_manager.py#PluginManager": {
132
+ "baseline_code_signature_text": "class PluginManager:",
133
+ "baseline_code_structure_hash": "8d211ba7f4db76c8fa4afc820902583d5f7df5c7e482f99b988dba61ece4a2f5",
134
+ "baseline_yaml_content_hash": "c22797fdf50ccd3ac64731ca93183e2f70839b0611a049639bdc77b4f8fe7f15"
135
+ },
136
+ "py://packages/quipu-application/src/pyquipu/application/plugin_manager.py#PluginManager.load_from_sources": {
137
+ "baseline_code_signature_text": "def load_from_sources(self, executor: Executor, work_dir: Path):",
138
+ "baseline_code_structure_hash": "e1015eaa8d17516183baa58ec0239c567900a308ec70f60b9ce80fa9e1c72fd4",
139
+ "baseline_yaml_content_hash": "34b1c59e7fa9a03d089fccdafa494a216e5a7746e0aff3b5891a262b88986369"
140
+ },
141
+ "py://packages/quipu-application/src/pyquipu/application/utils.py#find_git_repository_root": {
142
+ "baseline_code_signature_text": "def find_git_repository_root(start_path: Path) -> Optional[Path]:",
143
+ "baseline_code_structure_hash": "9f19014baa92289d9b4f04d4ee8a92df475d31c38820edc4ffc20efc9ce218a8",
144
+ "baseline_yaml_content_hash": "8ab446ed5e94789aede646cc369436537ffdd64fc5da3229937ba99d5bc83bb7"
145
+ },
146
+ "py://packages/quipu-application/src/pyquipu/application/utils.py#logger": {},
147
+ "py://packages/quipu-application/tests/unit/conftest.py#__all__": {},
148
+ "py://packages/quipu-application/tests/unit/test_controller.py#TestControllerUnit": {
149
+ "baseline_code_signature_text": "class TestControllerUnit:",
150
+ "baseline_code_structure_hash": "b8668020e45ac212142e3e2194f2ce94369784ba3969a4757515a4059960e650",
151
+ "baseline_yaml_content_hash": "e2a45f750adc799b997d14d1c706d2542d3856ce9aba7773a49c5ba2ab15f1d3"
152
+ },
153
+ "py://packages/quipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_empty_plan": {
154
+ "baseline_code_signature_text": "def test_run_quipu_empty_plan(self, tmp_path, mock_engine, mock_runtime):",
155
+ "baseline_code_structure_hash": "d5d7459ace504109a1e35f058714a51eb8119c0ea9051168960d877179082f55",
156
+ "baseline_yaml_content_hash": "1071d0c4058fdf8a5b23344da1028be9c9f008be34eb421479e19174b14c4b6a"
157
+ },
158
+ "py://packages/quipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_execution_error": {
159
+ "baseline_code_signature_text": "def test_run_quipu_execution_error(self, tmp_path, mock_engine, mock_runtime):",
160
+ "baseline_code_structure_hash": "97f2e3585c826243e7b61c6b4e3ae485f9986b81c50ef6c72058494a5b12960b",
161
+ "baseline_yaml_content_hash": "b8ffd9eec97c064fabd6851169bed6211c276c164ec09f3747a2203127b28bf4"
162
+ },
163
+ "py://packages/quipu-application/tests/unit/test_controller.py#TestControllerUnit.test_run_quipu_success": {
164
+ "baseline_code_signature_text": "def test_run_quipu_success(self, tmp_path, mock_engine, mock_runtime):",
165
+ "baseline_code_structure_hash": "eed582f286876d423157f91ac81d1e7963b67cf3eaf66750e14526adf7832146",
166
+ "baseline_yaml_content_hash": "51d669c5b333dde1c9c4080b4e32e99ee7bfc81f069c896b5dc234b543787810"
167
+ },
168
+ "py://packages/quipu-application/tests/unit/test_utils.py#TestRootDiscovery": {
169
+ "baseline_code_signature_text": "class TestRootDiscovery:",
170
+ "baseline_code_structure_hash": "3b99ed1f64e93ef2a1c0050627ddd0d49734803e0b2e6f16697299dd446a4888"
171
+ },
172
+ "py://packages/quipu-application/tests/unit/test_utils.py#TestRootDiscovery.test_find_git_repository_root": {
173
+ "baseline_code_signature_text": "def test_find_git_repository_root(self, tmp_path: Path):",
174
+ "baseline_code_structure_hash": "512797d245dc49e41a5d3c7c638481b2b0bc3efa3c6be7b89b058842beea7641"
175
+ }
176
+ },
177
+ "version": "1.0"
178
+ }
@@ -0,0 +1,3 @@
1
+ from quipu.test_utils.fixtures import mock_engine, mock_runtime
2
+
3
+ __all__ = ["mock_engine", "mock_runtime"]
@@ -0,0 +1,100 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ from quipu.application.controller import run_quipu
4
+ from quipu.interfaces.exceptions import ExecutionError
5
+
6
+
7
+ class TestControllerUnit:
8
+ def test_run_quipu_success(self, tmp_path, mock_engine, mock_runtime):
9
+ plan_content = """
10
+ ```act
11
+ echo
12
+ ```
13
+ ```text
14
+ hello
15
+ ```
16
+ """
17
+ # 配置 Mock Engine 的状态以通过 _prepare_workspace 检查
18
+ # 模拟当前是干净状态 (clean)
19
+ mock_engine.git_db.get_tree_hash.return_value = "hash_123"
20
+ mock_node = MagicMock()
21
+ mock_node.output_tree = "hash_123"
22
+ mock_engine.current_node = mock_node
23
+ mock_engine.history_graph = {"hash_123": mock_node}
24
+
25
+ # Patch 工厂函数和 Executor 类
26
+ # 注意:controller 直接导入了 Executor 类,所以我们要 patch 这个类
27
+ with (
28
+ patch("quipu.application.controller.create_engine", return_value=mock_engine) as mk_eng_fac,
29
+ patch("quipu.application.controller.Executor", return_value=mock_runtime) as mk_exec_cls,
30
+ ):
31
+ # 执行
32
+ result = run_quipu(content=plan_content, work_dir=tmp_path, yolo=True, confirmation_handler=lambda *a: True)
33
+
34
+ # 验证结果
35
+ assert result.success is True
36
+ assert result.exit_code == 0
37
+
38
+ # 验证交互
39
+ mk_eng_fac.assert_called_once_with(tmp_path)
40
+ # Executor 类被实例化
41
+ mk_exec_cls.assert_called_once()
42
+
43
+ # 验证编排顺序
44
+ # 1. _prepare_workspace 调用了 get_tree_hash
45
+ mock_engine.git_db.get_tree_hash.assert_called()
46
+
47
+ # 2. Executor 执行
48
+ mock_runtime.execute.assert_called_once()
49
+
50
+ # 3. 最后生成 Plan Node
51
+ mock_engine.create_plan_node.assert_called_once()
52
+
53
+ def test_run_quipu_execution_error(self, tmp_path, mock_engine, mock_runtime):
54
+ plan_content = """
55
+ ```act
56
+ fail_act
57
+ ```
58
+ """
59
+ # 配置 Mock Engine
60
+ mock_engine.git_db.get_tree_hash.return_value = "hash_123"
61
+ mock_engine.current_node = MagicMock()
62
+ mock_engine.current_node.output_tree = "hash_123"
63
+
64
+ with (
65
+ patch("quipu.application.controller.create_engine", return_value=mock_engine),
66
+ patch("quipu.application.controller.Executor", return_value=mock_runtime),
67
+ ):
68
+ # 模拟 Runtime 抛出业务异常
69
+ mock_runtime.execute.side_effect = ExecutionError("Task failed successfully")
70
+
71
+ result = run_quipu(content=plan_content, work_dir=tmp_path, yolo=True, confirmation_handler=lambda *a: True)
72
+
73
+ # 验证错误被捕获并封装到 Result 中
74
+ assert result.success is False
75
+ assert result.exit_code == 1
76
+ assert result.message == "run.error.execution"
77
+ assert isinstance(result.error, ExecutionError)
78
+ assert "Task failed successfully" in str(result.error)
79
+
80
+ def test_run_quipu_empty_plan(self, tmp_path, mock_engine, mock_runtime):
81
+ plan_content = "Just some text, no acts."
82
+
83
+ # 配置 Mock Engine
84
+ mock_engine.git_db.get_tree_hash.return_value = "hash_123"
85
+ mock_engine.current_node = MagicMock()
86
+ mock_engine.current_node.output_tree = "hash_123"
87
+
88
+ with (
89
+ patch("quipu.application.controller.create_engine", return_value=mock_engine),
90
+ patch("quipu.application.controller.Executor", return_value=mock_runtime),
91
+ ):
92
+ result = run_quipu(content=plan_content, work_dir=tmp_path, yolo=True, confirmation_handler=lambda *a: True)
93
+
94
+ # 空计划通常不算失败,但也没有副作用
95
+ assert result.success is True
96
+ assert result.exit_code == 0
97
+ assert result.message == "axon.warning.noStatements"
98
+
99
+ # 验证没有调用 execute
100
+ mock_runtime.execute.assert_not_called()
@@ -0,0 +1,9 @@
1
+ "TestControllerUnit": |-
2
+ 对 Application 层 Controller 的纯单元测试。
3
+ 使用 Mock 替代真实的 Engine 和 Runtime,仅验证编排逻辑。
4
+ "TestControllerUnit.test_run_quipu_empty_plan": |-
5
+ 测试空计划的处理。
6
+ "TestControllerUnit.test_run_quipu_execution_error": |-
7
+ 测试执行器抛出异常时的错误处理流程。
8
+ "TestControllerUnit.test_run_quipu_success": |-
9
+ 测试正常执行流程:应正确初始化组件并按顺序调用。
@@ -0,0 +1,26 @@
1
+ from pathlib import Path
2
+
3
+ from quipu.application.utils import find_git_repository_root
4
+
5
+
6
+ class TestRootDiscovery:
7
+ def test_find_git_repository_root(self, tmp_path: Path):
8
+ # /project/.git
9
+ # /project/src/subdir
10
+ project = tmp_path / "project"
11
+ project.mkdir()
12
+ (project / ".git").mkdir()
13
+
14
+ subdir = project / "src" / "subdir"
15
+ subdir.mkdir(parents=True)
16
+
17
+ # Case 1: From subdir
18
+ assert find_git_repository_root(subdir) == project.resolve()
19
+
20
+ # Case 2: From root
21
+ assert find_git_repository_root(project) == project.resolve()
22
+
23
+ # Case 3: Outside
24
+ outside = tmp_path / "outside"
25
+ outside.mkdir()
26
+ assert find_git_repository_root(outside) is None