workflow-loop 0.3.7__tar.gz → 0.3.9__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.
- {workflow_loop-0.3.7/src/workflow_loop.egg-info → workflow_loop-0.3.9}/PKG-INFO +9 -9
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/README.md +8 -8
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/pyproject.toml +1 -1
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/__init__.py +2 -2
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/artifact_paths.py +23 -3
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/artifact_validation.py +148 -34
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/bug_record.py +122 -15
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/cli.py +252 -39
- workflow_loop-0.3.9/src/workflow_loop/code_symbols.py +145 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/code_design/update_code_design.md +9 -1
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/global/workflow_lifecycle.md +1 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/qa/test.md +3 -1
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/qa/test_code.md +1 -1
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/qa/test_code_implementation.md +1 -1
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/qa/test_plan.md +3 -3
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/reproduce/reproduce.md +7 -3
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/code_design/code_design.md +2 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/reproduce/reproduce.md +8 -1
- workflow_loop-0.3.9/src/workflow_loop/design_sync.py +209 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/installer.py +5 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/journal.py +20 -0
- workflow_loop-0.3.9/src/workflow_loop/machine_collect.py +641 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/markdown_links.py +176 -40
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/records.py +1116 -136
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/rollback.py +105 -8
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/stages/stages.py +46 -3
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/state.py +5 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/test_execution.py +39 -5
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/test_mapping.py +128 -15
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/test_runner.py +48 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/topic.py +6 -8
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/traceability.py +76 -3
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/verification.py +25 -2
- {workflow_loop-0.3.7 → workflow_loop-0.3.9/src/workflow_loop.egg-info}/PKG-INFO +9 -9
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop.egg-info/SOURCES.txt +19 -1
- workflow_loop-0.3.9/tests/test_design_sync_tables.py +215 -0
- workflow_loop-0.3.9/tests/test_empty_result_table_guard.py +71 -0
- workflow_loop-0.3.9/tests/test_gate_plan_path_diagnostics.py +290 -0
- workflow_loop-0.3.9/tests/test_gate_rework_stability.py +817 -0
- workflow_loop-0.3.9/tests/test_gate_self_heal_output.py +224 -0
- workflow_loop-0.3.9/tests/test_git_baseline_exit.py +171 -0
- workflow_loop-0.3.9/tests/test_machine_collect.py +437 -0
- workflow_loop-0.3.9/tests/test_machine_collect_extension.py +234 -0
- workflow_loop-0.3.9/tests/test_multibug_workflow.py +269 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_records.py +24 -10
- workflow_loop-0.3.9/tests/test_recovery_journal_boundary.py +136 -0
- workflow_loop-0.3.9/tests/test_registration_guidance.py +164 -0
- workflow_loop-0.3.9/tests/test_regression_skip.py +189 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_rollback.py +2 -1
- workflow_loop-0.3.9/tests/test_table_entry_registration.py +273 -0
- workflow_loop-0.3.9/tests/test_version_document_sources.py +70 -0
- workflow_loop-0.3.9/tests/test_workflow_guidance.py +588 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/LICENSE +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/setup.cfg +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/acceptance_records.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/acceptance/acceptance.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/acceptance/acceptance_plan.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/code_design/code_design.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/code_design/project_design_init.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/global/document_writing.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/impl/code_implementation.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/impl/impl.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/spec/spec.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Standardized_Repository/spike/spike.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/acceptance/acceptance_plan.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/acceptance/acceptance_result.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/code_design/project_design_init_evidence.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/impl/impl.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/qa/test.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/qa/test_plan.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/spec/spec.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/data/Template_Repository/spike/spike.md +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/diagnostics.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/path_composer.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/process_runner.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/project.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/role_doc.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/snapshots.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/spike_reuse.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/spike_validation.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/stage_materials.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/stages/__init__.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/stages/base.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/test_entry.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/test_report.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop/topic_relations.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop.egg-info/dependency_links.txt +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop.egg-info/entry_points.txt +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop.egg-info/requires.txt +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/src/workflow_loop.egg-info/top_level.txt +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_acceptance_records.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_architecture_validation.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_artifact_paths.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_bug_record.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_bug_validation.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_commands.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_current_workflow_contracts.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_current_workflow_qa.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_diagnostics.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_installer.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_light_task.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_maintenance.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_maintenance_scripts.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_markdown_links.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_path_composer.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_process_runner.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_project.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_project_design_init.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_public_project.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_release_script.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_release_workflow.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_return_rework_flow.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_snapshots.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_spike_assets.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_spike_validation.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_stage_materials.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_stages.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_state.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_test_entry.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_test_execution.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_test_mapping.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_test_report.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_test_runner.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_topic_relations.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_traceability.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_verification.py +0 -0
- {workflow_loop-0.3.7 → workflow_loop-0.3.9}/tests/test_workflow_acceptance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workflow-loop
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.9
|
|
4
4
|
Summary: 为 AI 驱动的软件开发提供有状态、可验证、可回退的工作流管理。
|
|
5
5
|
Author: yuzyf
|
|
6
6
|
License-Expression: MIT
|
|
@@ -95,29 +95,29 @@ flowchart TD
|
|
|
95
95
|
- 安装时能够访问 GitHub 和 PyPI(Python 公共软件包仓库)。
|
|
96
96
|
- 执行安装命令前,先进入要由 Workflow Loop 管理的项目根目录。
|
|
97
97
|
|
|
98
|
-
## 安装 0.3.
|
|
98
|
+
## 安装 0.3.9
|
|
99
99
|
|
|
100
100
|
安装器先进行只读检查并列出项目侧和电脑侧可能发生的全部持久修改。用户确认一次后,安装器才安装或复用全局 `workflow` 命令,并把智能体契约、产物模板和工作规范写入当前项目;用户取消或安装失败时不会留下只完成一部分的安装。
|
|
101
101
|
|
|
102
102
|
### macOS
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
curl -fsSL https://github.com/yuzyf/workflow_loop/releases/download/v0.3.
|
|
105
|
+
curl -fsSL https://github.com/yuzyf/workflow_loop/releases/download/v0.3.9/install.sh | bash
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
### Linux
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
|
-
curl -fsSL https://github.com/yuzyf/workflow_loop/releases/download/v0.3.
|
|
111
|
+
curl -fsSL https://github.com/yuzyf/workflow_loop/releases/download/v0.3.9/install.sh | bash
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
### 原生 Windows
|
|
115
115
|
|
|
116
116
|
```powershell
|
|
117
|
-
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://github.com/yuzyf/workflow_loop/releases/download/v0.3.
|
|
117
|
+
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://github.com/yuzyf/workflow_loop/releases/download/v0.3.9/install.ps1 | iex"
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
安装命令固定读取 `v0.3.
|
|
120
|
+
安装命令固定读取 `v0.3.9` 正式发布中的脚本;不会跟随内容可能变化的 `latest`(最新版本)地址。
|
|
121
121
|
|
|
122
122
|
## 更新与卸载
|
|
123
123
|
|
|
@@ -125,7 +125,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://github.com/y
|
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
127
|
workflow update
|
|
128
|
-
workflow update --version 0.3.
|
|
128
|
+
workflow update --version 0.3.9
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
更新按需补齐电脑全局命令和当前项目,只直接覆盖项目根 `AGENTS.md`、`.workflow_loop/Template_Repository/`、`.workflow_loop/Standardized_Repository/`,以及 `.workflow_loop/project.json` 中的安装版本字段。更新不创建备份,不回滚已经完成的步骤;当前轮次状态、历史、回退资料、业务代码和正式产物保持不变。失败后重新执行同一命令即可继续补齐。
|
|
@@ -229,10 +229,10 @@ uv run python -m build
|
|
|
229
229
|
|
|
230
230
|
## 发布正式版本
|
|
231
231
|
|
|
232
|
-
项目维护者确认当前仓库内容可以直接发布后,在仓库根目录执行下面的命令,并把 `0.3.
|
|
232
|
+
项目维护者确认当前仓库内容可以直接发布后,在仓库根目录执行下面的命令,并把 `0.3.9` 替换成要发布的新版本号:
|
|
233
233
|
|
|
234
234
|
```bash
|
|
235
|
-
uv run python scripts/release.py 0.3.
|
|
235
|
+
uv run python scripts/release.py 0.3.9
|
|
236
236
|
```
|
|
237
237
|
|
|
238
238
|
运行这条命令本身就表示维护者接受当前仓库状态并承担跳过本地发布检查的风险。脚本不会再次确认,也不会在本地运行测试、构建或远程版本查询。它依次执行:
|
|
@@ -76,29 +76,29 @@ flowchart TD
|
|
|
76
76
|
- 安装时能够访问 GitHub 和 PyPI(Python 公共软件包仓库)。
|
|
77
77
|
- 执行安装命令前,先进入要由 Workflow Loop 管理的项目根目录。
|
|
78
78
|
|
|
79
|
-
## 安装 0.3.
|
|
79
|
+
## 安装 0.3.9
|
|
80
80
|
|
|
81
81
|
安装器先进行只读检查并列出项目侧和电脑侧可能发生的全部持久修改。用户确认一次后,安装器才安装或复用全局 `workflow` 命令,并把智能体契约、产物模板和工作规范写入当前项目;用户取消或安装失败时不会留下只完成一部分的安装。
|
|
82
82
|
|
|
83
83
|
### macOS
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
curl -fsSL https://github.com/yuzyf/workflow_loop/releases/download/v0.3.
|
|
86
|
+
curl -fsSL https://github.com/yuzyf/workflow_loop/releases/download/v0.3.9/install.sh | bash
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
### Linux
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
curl -fsSL https://github.com/yuzyf/workflow_loop/releases/download/v0.3.
|
|
92
|
+
curl -fsSL https://github.com/yuzyf/workflow_loop/releases/download/v0.3.9/install.sh | bash
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
### 原生 Windows
|
|
96
96
|
|
|
97
97
|
```powershell
|
|
98
|
-
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://github.com/yuzyf/workflow_loop/releases/download/v0.3.
|
|
98
|
+
powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://github.com/yuzyf/workflow_loop/releases/download/v0.3.9/install.ps1 | iex"
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
安装命令固定读取 `v0.3.
|
|
101
|
+
安装命令固定读取 `v0.3.9` 正式发布中的脚本;不会跟随内容可能变化的 `latest`(最新版本)地址。
|
|
102
102
|
|
|
103
103
|
## 更新与卸载
|
|
104
104
|
|
|
@@ -106,7 +106,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://github.com/y
|
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
108
|
workflow update
|
|
109
|
-
workflow update --version 0.3.
|
|
109
|
+
workflow update --version 0.3.9
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
更新按需补齐电脑全局命令和当前项目,只直接覆盖项目根 `AGENTS.md`、`.workflow_loop/Template_Repository/`、`.workflow_loop/Standardized_Repository/`,以及 `.workflow_loop/project.json` 中的安装版本字段。更新不创建备份,不回滚已经完成的步骤;当前轮次状态、历史、回退资料、业务代码和正式产物保持不变。失败后重新执行同一命令即可继续补齐。
|
|
@@ -210,10 +210,10 @@ uv run python -m build
|
|
|
210
210
|
|
|
211
211
|
## 发布正式版本
|
|
212
212
|
|
|
213
|
-
项目维护者确认当前仓库内容可以直接发布后,在仓库根目录执行下面的命令,并把 `0.3.
|
|
213
|
+
项目维护者确认当前仓库内容可以直接发布后,在仓库根目录执行下面的命令,并把 `0.3.9` 替换成要发布的新版本号:
|
|
214
214
|
|
|
215
215
|
```bash
|
|
216
|
-
uv run python scripts/release.py 0.3.
|
|
216
|
+
uv run python scripts/release.py 0.3.9
|
|
217
217
|
```
|
|
218
218
|
|
|
219
219
|
运行这条命令本身就表示维护者接受当前仓库状态并承担跳过本地发布检查的风险。脚本不会再次确认,也不会在本地运行测试、构建或远程版本查询。它依次执行:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 包版本号,用于 pip/uv 安装时版本匹配
|
|
2
|
-
__version__ = "0.3.
|
|
2
|
+
__version__ = "0.3.9"
|
|
3
3
|
# 产品身份标识:workflow --version 输出、安装脚本身份核对和安装事务校验共用同一组常量
|
|
4
|
-
# 本次发布版本为 0.3.
|
|
4
|
+
# 本次发布版本为 0.3.9;后续发布使用尚未被 PyPI 占用的新版本号
|
|
5
5
|
PRODUCT_NAME = "workflow-loop"
|
|
6
6
|
PRODUCT_IDENTITY = f"{PRODUCT_NAME} {__version__}"
|
|
@@ -82,15 +82,23 @@ def resolve_file_key(
|
|
|
82
82
|
saved_keys: dict[str, str],
|
|
83
83
|
all_used_keys: set[str],
|
|
84
84
|
display_name: str,
|
|
85
|
+
keys_by_name_all: dict[str, str] | None = None,
|
|
85
86
|
) -> str:
|
|
86
87
|
"""在一个分类内解析显示名称的文件标识。
|
|
87
88
|
|
|
88
89
|
saved_keys 是该分类已保存的 显示名称→文件标识 映射;all_used_keys 是全部
|
|
89
|
-
|
|
90
|
+
分类已占用的标识(大小写不敏感比较,避免只在大小写上不同的路径冲突);
|
|
91
|
+
keys_by_name_all 是全部分类的 显示名称→文件标识 映射。
|
|
90
92
|
已保存的名称直接返回旧标识;新名称生成标识,冲突时依次追加 `_2`、`_3`。
|
|
93
|
+
|
|
94
|
+
同一显示名称在不同分类表示同一件事(缺陷名与验收主题名相同是常态),
|
|
95
|
+
因此先在全部分类中按名称复用同一标识;否则第二个分类必然被迫追加 `_2`,
|
|
96
|
+
使生成路径与登记路径分叉。不同名称之间仍不允许共用标识。
|
|
91
97
|
"""
|
|
92
98
|
if display_name in saved_keys:
|
|
93
99
|
return saved_keys[display_name]
|
|
100
|
+
if keys_by_name_all is not None and display_name in keys_by_name_all:
|
|
101
|
+
return keys_by_name_all[display_name]
|
|
94
102
|
base_key = make_file_key(display_name)
|
|
95
103
|
used_lower = {key.lower() for key in all_used_keys}
|
|
96
104
|
candidate = base_key
|
|
@@ -115,12 +123,13 @@ def register_file_keys(project, category: str, display_names: list[str]) -> dict
|
|
|
115
123
|
for mapping in project.artifact_file_keys.values()
|
|
116
124
|
for key in mapping.values()
|
|
117
125
|
}
|
|
126
|
+
keys_by_name_all = _keys_by_name(project)
|
|
118
127
|
reverse = {key: name for name, key in keys.items()}
|
|
119
128
|
added: dict[str, str] = {}
|
|
120
129
|
for display_name in display_names:
|
|
121
130
|
if display_name in keys:
|
|
122
131
|
continue
|
|
123
|
-
key = resolve_file_key(keys, all_used, display_name)
|
|
132
|
+
key = resolve_file_key(keys, all_used, display_name, keys_by_name_all)
|
|
124
133
|
owner = reverse.get(key)
|
|
125
134
|
if owner is not None and owner != display_name:
|
|
126
135
|
raise ValueError(
|
|
@@ -129,10 +138,21 @@ def register_file_keys(project, category: str, display_names: list[str]) -> dict
|
|
|
129
138
|
keys[display_name] = key
|
|
130
139
|
reverse[key] = display_name
|
|
131
140
|
all_used.add(key)
|
|
141
|
+
keys_by_name_all[display_name] = key
|
|
132
142
|
added[display_name] = key
|
|
133
143
|
return added
|
|
134
144
|
|
|
135
145
|
|
|
146
|
+
def _keys_by_name(project) -> dict[str, str]:
|
|
147
|
+
"""全部分类的 显示名称→文件标识 映射,供同名跨分类复用同一标识。"""
|
|
148
|
+
if project is None:
|
|
149
|
+
return {}
|
|
150
|
+
merged: dict[str, str] = {}
|
|
151
|
+
for mapping in project.artifact_file_keys.values():
|
|
152
|
+
merged.update(mapping)
|
|
153
|
+
return merged
|
|
154
|
+
|
|
155
|
+
|
|
136
156
|
def lookup_file_key(project, category: str, display_name: str) -> str | None:
|
|
137
157
|
"""读取已保存的文件标识;没有登记时返回 None。"""
|
|
138
158
|
if project is None:
|
|
@@ -152,7 +172,7 @@ def resolve_key_for(project, category: str, display_name: str) -> str:
|
|
|
152
172
|
if project is not None
|
|
153
173
|
else set()
|
|
154
174
|
)
|
|
155
|
-
return resolve_file_key(saved, all_used, display_name)
|
|
175
|
+
return resolve_file_key(saved, all_used, display_name, _keys_by_name(project))
|
|
156
176
|
|
|
157
177
|
|
|
158
178
|
# ─── 按文件标识生成动态正式产物路径 ───
|
|
@@ -233,20 +233,47 @@ def _markdown_tables(content: str) -> list[tuple[list[str], list[list[str]]]]:
|
|
|
233
233
|
return tables
|
|
234
234
|
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
# 模板占位写法:整行被尖括号包住,例如 `<用户在什么操作中看到了什么问题。>`。
|
|
237
|
+
# 只按整行判定;正文里合法出现的半角小于号(路径占位写法、HTML 锚点、比较运算)
|
|
238
|
+
# 不影响判定,否则含实质内容的长章节会被整段误判为未填写。
|
|
239
|
+
_TEMPLATE_PLACEHOLDER_LINE_RE = re.compile(r"^<[^<>]*>$")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _placeholder_lines(value: str) -> list[str]:
|
|
243
|
+
"""返回值中仍是模板占位写法的行;全部非空行都命中才算整体未填写。"""
|
|
244
|
+
stripped_lines = [
|
|
245
|
+
re.sub(r"^[-*]\s+", "", line).strip()
|
|
246
|
+
for line in value.splitlines()
|
|
247
|
+
if line.strip()
|
|
248
|
+
]
|
|
249
|
+
if not stripped_lines:
|
|
250
|
+
return []
|
|
251
|
+
hits = [line for line in stripped_lines if _TEMPLATE_PLACEHOLDER_LINE_RE.match(line)]
|
|
252
|
+
return hits if len(hits) == len(stripped_lines) else []
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _no_real_text_reason(value: str | None, *, allow_none: bool = False) -> str | None:
|
|
256
|
+
"""值不算已填写时返回具体原因和命中内容;已填写时返回 None。"""
|
|
237
257
|
if value is None:
|
|
238
|
-
return
|
|
258
|
+
return "缺少这一节"
|
|
239
259
|
normalized = value.strip()
|
|
240
260
|
if not normalized:
|
|
241
|
-
return
|
|
261
|
+
return "内容为空"
|
|
242
262
|
if allow_none and normalized in {"无", "暂无"}:
|
|
243
|
-
return
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
263
|
+
return None
|
|
264
|
+
if normalized in {"无", "暂无", "待补充", "未填写"}:
|
|
265
|
+
return f"整段只写了占位词 {normalized!r}"
|
|
266
|
+
if "todo" in normalized.lower():
|
|
267
|
+
return "内容里仍留着 TODO 标记"
|
|
268
|
+
hits = _placeholder_lines(normalized)
|
|
269
|
+
if hits:
|
|
270
|
+
sample = hits[0] if len(hits[0]) <= 40 else hits[0][:40] + "…"
|
|
271
|
+
return f"整段仍是模板占位写法,例如 {sample}"
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _has_real_text(value: str | None, *, allow_none: bool = False) -> bool:
|
|
276
|
+
return _no_real_text_reason(value, allow_none=allow_none) is None
|
|
250
277
|
|
|
251
278
|
|
|
252
279
|
def _format_validation_failures(failures: list[str]) -> str:
|
|
@@ -587,6 +614,10 @@ def _required_final_machine_record_ids(wf_state) -> tuple[set[str] | None, str]:
|
|
|
587
614
|
required.update(record.test_record_ids)
|
|
588
615
|
|
|
589
616
|
regression = wf_state.regression_test
|
|
617
|
+
if regression.status == "skipped-by-no-change":
|
|
618
|
+
# R9/R9a:无代码改动跳过的轮次没有机器执行记录;设计同步照常,
|
|
619
|
+
# 结论按真实差异为无需修改,不要求不存在的 REG 编号。
|
|
620
|
+
return required, ""
|
|
590
621
|
if regression.status != "passed" or not regression.record_id:
|
|
591
622
|
return None, "最终全量回归没有当前有效的机器记录编号"
|
|
592
623
|
required.add(regression.record_id)
|
|
@@ -609,7 +640,12 @@ def changed_stage_paths(
|
|
|
609
640
|
stage_name: str,
|
|
610
641
|
current_paths: list[str],
|
|
611
642
|
) -> tuple[bool, str, list[str]]:
|
|
612
|
-
"""比较讨论完成时的基线,返回本阶段新增、修改或删除的文件。
|
|
643
|
+
"""比较讨论完成时的基线,返回本阶段新增、修改或删除的文件。
|
|
644
|
+
|
|
645
|
+
启用工作记录表的轮次里正式文档是程序按表的确定性输出:表没有变化时文档必然
|
|
646
|
+
逐字节相同,"文档没变"不等于"本阶段没有产出"。这种情况下按表判断产出,并把
|
|
647
|
+
本阶段登记的产物路径交给下游检查,不要求 AI 制造无实质内容的修改。
|
|
648
|
+
"""
|
|
613
649
|
state = load_state(project_root)
|
|
614
650
|
if state is None or stage_name not in state.stages:
|
|
615
651
|
return (False, "找不到当前工作流阶段状态,无法判断文件是否属于本次工作", [])
|
|
@@ -627,6 +663,19 @@ def changed_stage_paths(
|
|
|
627
663
|
if baseline_hashes.get(rel_path) != current_hashes.get(rel_path)
|
|
628
664
|
]
|
|
629
665
|
if not changed:
|
|
666
|
+
from . import records as records_mod
|
|
667
|
+
|
|
668
|
+
if records_mod.stage_table_kinds(stage_name) and records_mod.workflow_uses_tables(
|
|
669
|
+
state, project_root
|
|
670
|
+
):
|
|
671
|
+
# 变化清单如实为空:确实没有文件变化。调用方要判断“本阶段的产出是哪些”时
|
|
672
|
+
# 按内容识别,不能把未变化的文件当成本阶段改过——最终设计同步正是用这份
|
|
673
|
+
# 清单确认产品文档没有被改动。
|
|
674
|
+
return (
|
|
675
|
+
True,
|
|
676
|
+
"表模式:正式文档由程序按工作记录表生成,内容与上次相同时按表判断产出",
|
|
677
|
+
[],
|
|
678
|
+
)
|
|
630
679
|
return (False, "相关文件与讨论完成时相同,不能证明本阶段已经生成或修改产物", [])
|
|
631
680
|
return (True, f"本阶段发生变化的文件: {changed}", changed)
|
|
632
681
|
|
|
@@ -1218,6 +1267,8 @@ def validate_project_design_feature_consistency(project_root: str) -> tuple[bool
|
|
|
1218
1267
|
def validate_final_code_design_document(
|
|
1219
1268
|
project_root: str,
|
|
1220
1269
|
workflow_id: str,
|
|
1270
|
+
*,
|
|
1271
|
+
candidate_content: str | None = None,
|
|
1221
1272
|
) -> tuple[bool, str]:
|
|
1222
1273
|
"""校验最终架构文档已经完成产品、功能、架构和真实代码映射。
|
|
1223
1274
|
|
|
@@ -1227,8 +1278,8 @@ def validate_final_code_design_document(
|
|
|
1227
1278
|
relative_path = artifact_paths_mod.CODE_DESIGN_DOC
|
|
1228
1279
|
full_path = os.path.join(project_root, relative_path)
|
|
1229
1280
|
errors: list[str] = []
|
|
1230
|
-
architecture_exists = os.path.isfile(full_path)
|
|
1231
|
-
content = _read_text(project_root, relative_path) if architecture_exists else ""
|
|
1281
|
+
architecture_exists = candidate_content is not None or os.path.isfile(full_path)
|
|
1282
|
+
content = candidate_content if candidate_content is not None else (_read_text(project_root, relative_path) if architecture_exists else "")
|
|
1232
1283
|
if not architecture_exists:
|
|
1233
1284
|
errors.append(f"{relative_path} 不存在")
|
|
1234
1285
|
|
|
@@ -1393,7 +1444,9 @@ def validate_final_code_design_document(
|
|
|
1393
1444
|
extra_ids = sorted(actual_record_ids - required_record_ids)
|
|
1394
1445
|
errors.append(
|
|
1395
1446
|
f"{relative_path} 第 9 章“核对依据”字段必须精确列出当前机器记录编号;"
|
|
1396
|
-
f"
|
|
1447
|
+
f"当前缺少={missing_ids},不在当前有效集合中的编号={extra_ids}。"
|
|
1448
|
+
"测试或最终全量回归重跑会使旧编号失效;额外编号也可能是拼写或拼接错误,"
|
|
1449
|
+
"请从当前工作流状态重新取得有效编号集合,不要沿用历史编号"
|
|
1397
1450
|
)
|
|
1398
1451
|
|
|
1399
1452
|
if errors:
|
|
@@ -1405,6 +1458,21 @@ def validate_final_code_design_document(
|
|
|
1405
1458
|
)
|
|
1406
1459
|
|
|
1407
1460
|
|
|
1461
|
+
def _current_workflow_bug_documents(project_root: str, workflow_id: str) -> list[str]:
|
|
1462
|
+
"""按文件内的工作流编号找出本轮的缺陷记录,不依赖本阶段变化清单。"""
|
|
1463
|
+
bug_dir = os.path.join(project_root, "bug")
|
|
1464
|
+
if not os.path.isdir(bug_dir):
|
|
1465
|
+
return []
|
|
1466
|
+
found: list[str] = []
|
|
1467
|
+
for filename in sorted(os.listdir(bug_dir)):
|
|
1468
|
+
if not filename.endswith(".md") or filename in BUG_INDEX_FILENAMES:
|
|
1469
|
+
continue
|
|
1470
|
+
relative = f"bug/{filename}"
|
|
1471
|
+
if _field(_read_text(project_root, relative), "工作流编号") == workflow_id:
|
|
1472
|
+
found.append(relative)
|
|
1473
|
+
return found
|
|
1474
|
+
|
|
1475
|
+
|
|
1408
1476
|
def validate_reproduce_documents(
|
|
1409
1477
|
project_root: str,
|
|
1410
1478
|
changed_paths: list[str],
|
|
@@ -1413,8 +1481,8 @@ def validate_reproduce_documents(
|
|
|
1413
1481
|
index_path = artifact_paths_mod.BUG_INDEX_DOC
|
|
1414
1482
|
normalized_changed = [path.replace(os.sep, "/") for path in changed_paths]
|
|
1415
1483
|
failures: list[str] = []
|
|
1416
|
-
|
|
1417
|
-
|
|
1484
|
+
# 索引按内容判定:存在且链接本轮缺陷记录即可。索引由程序生成,内容已经正确时
|
|
1485
|
+
# 本阶段不需要再改它,"这一轮没有改过"不是问题。
|
|
1418
1486
|
index_exists = os.path.isfile(os.path.join(project_root, index_path))
|
|
1419
1487
|
if not index_exists:
|
|
1420
1488
|
failures.append(f"{index_path} 不存在")
|
|
@@ -1438,6 +1506,10 @@ def validate_reproduce_documents(
|
|
|
1438
1506
|
for rel_path in changed_bug_candidates
|
|
1439
1507
|
if os.path.isfile(os.path.join(project_root, rel_path))
|
|
1440
1508
|
]
|
|
1509
|
+
if not changed_bug_docs:
|
|
1510
|
+
# 表模式下缺陷记录由程序按表生成,重走本环节时内容可能与上次逐字节相同,
|
|
1511
|
+
# 变化清单为空不等于本轮没有缺陷记录。按文件内的工作流编号识别本轮记录。
|
|
1512
|
+
changed_bug_docs = _current_workflow_bug_documents(project_root, workflow_id)
|
|
1441
1513
|
if not changed_bug_docs:
|
|
1442
1514
|
failures.append("本阶段没有新增或修改缺陷记录文档")
|
|
1443
1515
|
|
|
@@ -1453,6 +1525,27 @@ def validate_reproduce_documents(
|
|
|
1453
1525
|
]
|
|
1454
1526
|
|
|
1455
1527
|
topics: list[str] = []
|
|
1528
|
+
from . import records as records_mod
|
|
1529
|
+
|
|
1530
|
+
workflow = load_state(project_root)
|
|
1531
|
+
uses_tables = workflow is not None and records_mod.workflow_uses_tables(workflow, project_root)
|
|
1532
|
+
fact_tables = {}
|
|
1533
|
+
table_problems, tables = records_mod.bug_record_tables(
|
|
1534
|
+
project_root, workflow_id, list(workflow.topics) if uses_tables else None,
|
|
1535
|
+
)
|
|
1536
|
+
failures.extend(detail for _, detail in table_problems)
|
|
1537
|
+
for table_relative, table in tables:
|
|
1538
|
+
fact_tables[table["验收主题"]] = table
|
|
1539
|
+
failures.extend(
|
|
1540
|
+
f"{table_relative}:{detail}" for _category, detail in
|
|
1541
|
+
records_mod.validate_table("bug_record", table, project_root=project_root)
|
|
1542
|
+
)
|
|
1543
|
+
if uses_tables:
|
|
1544
|
+
changed_bug_docs = list(dict.fromkeys(changed_bug_docs + _current_workflow_bug_documents(project_root, workflow_id)))
|
|
1545
|
+
for table in fact_tables.values():
|
|
1546
|
+
expected = f"bug/缺陷_{records_mod.bug_file_key(project_root, table['验收主题'])}.md"
|
|
1547
|
+
if expected not in changed_bug_docs:
|
|
1548
|
+
failures.append(f"主题「{table['验收主题']}」缺少本轮正式缺陷记录 {expected}")
|
|
1456
1549
|
for rel_path in changed_bug_docs:
|
|
1457
1550
|
filename = os.path.basename(rel_path)
|
|
1458
1551
|
if not BUG_FILENAME_RE.match(filename):
|
|
@@ -1483,9 +1576,21 @@ def validate_reproduce_documents(
|
|
|
1483
1576
|
else:
|
|
1484
1577
|
topics.append(topic or "")
|
|
1485
1578
|
|
|
1579
|
+
fact_table = fact_tables.get(topic)
|
|
1580
|
+
if uses_tables and fact_table is None:
|
|
1581
|
+
failures.append(f"{filename} 的主题「{topic}」缺少对应的缺陷记录工作记录表")
|
|
1582
|
+
if fact_table is not None and fact_table.get("表版本") == "3":
|
|
1583
|
+
for heading in required_sections:
|
|
1584
|
+
if _section(content, heading) is None:
|
|
1585
|
+
failures.append(f"{filename} 缺少“{heading}”章节")
|
|
1586
|
+
continue
|
|
1587
|
+
|
|
1486
1588
|
for heading in required_sections:
|
|
1487
|
-
|
|
1488
|
-
|
|
1589
|
+
allow_none = heading.startswith("7.")
|
|
1590
|
+
section_text = _section(content, heading)
|
|
1591
|
+
reason = _no_real_text_reason(section_text, allow_none=allow_none)
|
|
1592
|
+
if reason is not None:
|
|
1593
|
+
failures.append(f"{filename} 的“{heading}”缺少具体内容:{reason}")
|
|
1489
1594
|
|
|
1490
1595
|
condition_section = _section(content, "2. 真实复现条件") or ""
|
|
1491
1596
|
if not _has_real_text(_field(condition_section, "运行环境")):
|
|
@@ -1874,24 +1979,17 @@ def validate_downstream_traceability(
|
|
|
1874
1979
|
|
|
1875
1980
|
|
|
1876
1981
|
def _table_mode_qa(project_root: str, workflow_id: str, topics: list[str]) -> bool:
|
|
1877
|
-
"""
|
|
1982
|
+
"""本轮是否按测试工作记录表校验测试计划。
|
|
1983
|
+
|
|
1984
|
+
R11:只看开工时冻结在工作流状态里的表版本。表缺失、为空或解析失败时
|
|
1985
|
+
仍留在表模式,由表校验报出具体问题,不退回按文档正文逐字检查的旧方式。
|
|
1986
|
+
"""
|
|
1878
1987
|
from . import records as records_mod
|
|
1879
1988
|
|
|
1880
|
-
|
|
1989
|
+
state = load_state(project_root)
|
|
1990
|
+
if state is None or state.workflow_id != workflow_id:
|
|
1881
1991
|
return False
|
|
1882
|
-
|
|
1883
|
-
relative = records_mod.table_relative_path(
|
|
1884
|
-
project_root, workflow_id, "test_plan", topic
|
|
1885
|
-
)
|
|
1886
|
-
if not records_mod.table_exists(project_root, relative):
|
|
1887
|
-
return False
|
|
1888
|
-
try:
|
|
1889
|
-
table = records_mod.load_table(os.path.join(project_root, relative))
|
|
1890
|
-
except records_mod.RecordsError:
|
|
1891
|
-
return False
|
|
1892
|
-
if not (table.get("测试项") or table.get("测试范围说明")):
|
|
1893
|
-
return False
|
|
1894
|
-
return True
|
|
1992
|
+
return records_mod.workflow_uses_tables(state, project_root)
|
|
1895
1993
|
|
|
1896
1994
|
|
|
1897
1995
|
def _validate_test_plan_from_tables(
|
|
@@ -1910,7 +2008,18 @@ def _validate_test_plan_from_tables(
|
|
|
1910
2008
|
relative = records_mod.table_relative_path(
|
|
1911
2009
|
project_root, workflow_id, "test_plan", topic
|
|
1912
2010
|
)
|
|
1913
|
-
|
|
2011
|
+
# 表模式下缺表、空表和解析失败都在这里报具体问题;不退回文档检查(R11)。
|
|
2012
|
+
if not records_mod.table_exists(project_root, relative):
|
|
2013
|
+
failures.append(
|
|
2014
|
+
f"主题「{topic}」缺少测试计划工作记录表({relative});"
|
|
2015
|
+
"请执行 workflow scaffold 补齐后填写测试项,再重新门禁"
|
|
2016
|
+
)
|
|
2017
|
+
continue
|
|
2018
|
+
try:
|
|
2019
|
+
table = records_mod.load_table(os.path.join(project_root, relative))
|
|
2020
|
+
except records_mod.RecordsError as exc:
|
|
2021
|
+
failures.append(f"主题「{topic}」的测试计划工作记录表无法解析({relative}):{exc}")
|
|
2022
|
+
continue
|
|
1914
2023
|
rows = table.get("测试项", [])
|
|
1915
2024
|
if not rows:
|
|
1916
2025
|
# 纯人工主题:自动化验证由最终全量回归承担,验收条件在主题验收人工核对
|
|
@@ -2575,6 +2684,11 @@ def validate_final_regression_state(
|
|
|
2575
2684
|
return (False, "找不到当前工作流状态,不能确认最终全量回归")
|
|
2576
2685
|
result = state.regression_test
|
|
2577
2686
|
failures: list[str] = []
|
|
2687
|
+
if result.status == "skipped-by-no-change":
|
|
2688
|
+
# R9:无代码改动跳过的轮次按判定依据核对,不要求不存在的执行事实
|
|
2689
|
+
if not result.skip_reason or not result.skipped_at:
|
|
2690
|
+
return (False, "最终全量回归标记为无代码改动跳过,但缺少跳过依据或判定时间")
|
|
2691
|
+
return (True, f"最终全量回归按无代码改动跳过(依据已记录:{result.skipped_at})")
|
|
2578
2692
|
if result.status == "unavailable":
|
|
2579
2693
|
failures.append("最终全量回归明确失败:当前平台没有可执行的项目全量测试入口")
|
|
2580
2694
|
elif result.status != "passed":
|
|
@@ -4,6 +4,10 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from urllib.parse import unquote
|
|
8
|
+
|
|
9
|
+
from markdown_it import MarkdownIt
|
|
10
|
+
from markdown_it.common.utils import unescapeAll
|
|
7
11
|
|
|
8
12
|
from . import artifact_paths as artifact_paths_mod
|
|
9
13
|
from .state import load_state
|
|
@@ -77,28 +81,68 @@ def _replace_result_update(content: str, stage_label: str, workflow_id: str, bod
|
|
|
77
81
|
return content[: section_match.start(1)] + section + content[section_match.end(1) :]
|
|
78
82
|
|
|
79
83
|
|
|
84
|
+
def index_entry(content: str, filename: str) -> tuple[tuple[int, int], list[str]] | None:
|
|
85
|
+
"""定位一个缺陷索引行,返回原文行范围和单元格;重复入口直接拒绝。"""
|
|
86
|
+
found = []
|
|
87
|
+
row_range = None
|
|
88
|
+
cells: list[str] = []
|
|
89
|
+
matches = False
|
|
90
|
+
for token in MarkdownIt("commonmark").enable("table").parse(content):
|
|
91
|
+
if token.type == "tr_open":
|
|
92
|
+
row_range, cells, matches = token.map, [], False
|
|
93
|
+
elif token.type == "inline" and row_range is not None:
|
|
94
|
+
if not cells:
|
|
95
|
+
matches = any(
|
|
96
|
+
child.type == "link_open"
|
|
97
|
+
and unquote(child.attrGet("href") or "") in {filename, f"./{filename}"}
|
|
98
|
+
for child in token.children or []
|
|
99
|
+
)
|
|
100
|
+
cells.append(unescapeAll(token.content))
|
|
101
|
+
elif token.type == "tr_close":
|
|
102
|
+
if matches:
|
|
103
|
+
if len(cells) < 2:
|
|
104
|
+
raise ValueError(f"缺陷索引中 {filename} 的条目缺少入口或状态列")
|
|
105
|
+
found.append((tuple(row_range), cells))
|
|
106
|
+
row_range = None
|
|
107
|
+
if len(found) > 1:
|
|
108
|
+
raise ValueError(f"缺陷索引中 {filename} 有重复入口,不能确定应更新哪一行")
|
|
109
|
+
return found[0] if found else None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def replace_index_entry(content: str, entry: tuple[int, int], cells: list[str]) -> str:
|
|
113
|
+
lines = content.splitlines(keepends=True)
|
|
114
|
+
rendered = [cell.replace("\\", "\\\\").replace("|", "\\|").replace("\n", "<br>") for cell in cells]
|
|
115
|
+
lines[entry[0]:entry[1]] = ["| " + " | ".join(rendered) + " |\n"]
|
|
116
|
+
return "".join(lines)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def index_width(content: str) -> int:
|
|
120
|
+
"""旧索引可只有入口和状态两列;新增条目沿用其表格结构。"""
|
|
121
|
+
width = 0
|
|
122
|
+
for token in MarkdownIt("commonmark").enable("table").parse(content):
|
|
123
|
+
if token.type == "th_open":
|
|
124
|
+
width += 1
|
|
125
|
+
elif token.type == "thead_close":
|
|
126
|
+
if width not in {2, 4}:
|
|
127
|
+
raise ValueError("缺陷索引必须使用旧版两列或当前四列表格,不能覆盖未知表格结构")
|
|
128
|
+
return width
|
|
129
|
+
if content.strip():
|
|
130
|
+
raise ValueError("缺陷索引没有可解析的表格,保留原内容")
|
|
131
|
+
return 4
|
|
132
|
+
|
|
133
|
+
|
|
80
134
|
def _update_index_status(project_root: str, filename: str, status: str) -> None:
|
|
81
135
|
index_path = Path(project_root) / BUG_INDEX
|
|
82
136
|
if not index_path.is_file():
|
|
83
137
|
raise ValueError(f"{BUG_INDEX} 不存在,无法更新缺陷索引")
|
|
84
138
|
|
|
85
139
|
content = index_path.read_text(encoding="utf-8")
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
found = False
|
|
89
|
-
for index, line in enumerate(lines):
|
|
90
|
-
if not any(marker in line for marker in markers) or not line.strip().startswith("|"):
|
|
91
|
-
continue
|
|
92
|
-
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
93
|
-
if len(cells) < 2:
|
|
94
|
-
continue
|
|
95
|
-
cells[-1] = status
|
|
96
|
-
lines[index] = "| " + " | ".join(cells) + " |"
|
|
97
|
-
found = True
|
|
98
|
-
break
|
|
99
|
-
if not found:
|
|
140
|
+
entry = index_entry(content, filename)
|
|
141
|
+
if entry is None:
|
|
100
142
|
raise ValueError(f"{BUG_INDEX} 没有链接缺陷记录: {filename}")
|
|
101
|
-
|
|
143
|
+
row_range, cells = entry
|
|
144
|
+
cells[-1] = status
|
|
145
|
+
index_path.write_text(replace_index_entry(content, row_range, cells), encoding="utf-8")
|
|
102
146
|
|
|
103
147
|
|
|
104
148
|
def update_status(
|
|
@@ -136,6 +180,69 @@ def update_status(
|
|
|
136
180
|
return f"已更新缺陷状态“{status}”: {updated}"
|
|
137
181
|
|
|
138
182
|
|
|
183
|
+
# 各阶段追加到第 8 节的结论块标题,按流程顺序排列。退回时清除目标环节之后的块。
|
|
184
|
+
_RESULT_STAGE_LABELS = ("主题验收结果", "最终全量回归结果", "整体验收确认")
|
|
185
|
+
# 退回目标 → 该目标之后失效的结论块起点下标
|
|
186
|
+
_RETURN_TARGET_FIRST_INVALID = {
|
|
187
|
+
"spec": 0,
|
|
188
|
+
"reproduce": 0,
|
|
189
|
+
"spike": 0,
|
|
190
|
+
"acceptance_plan": 0,
|
|
191
|
+
"impl": 0,
|
|
192
|
+
"qa": 0,
|
|
193
|
+
"test_plan": 0,
|
|
194
|
+
"test_code": 0,
|
|
195
|
+
"test_execution": 0,
|
|
196
|
+
"topic_acceptance": 0,
|
|
197
|
+
"regression_test": 1,
|
|
198
|
+
"overall_acceptance": 2,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def reset_status_for_return(
|
|
203
|
+
project_root: str,
|
|
204
|
+
workflow_id: str,
|
|
205
|
+
topics: list[str],
|
|
206
|
+
target_stage: str,
|
|
207
|
+
) -> str:
|
|
208
|
+
"""退回时清除已经失效的阶段结论块,并把缺陷索引状态改回复现阶段状态。
|
|
209
|
+
|
|
210
|
+
这些结论块由程序追加,块内链接指向测试结果和验收结果文档。退回会删除那些
|
|
211
|
+
文档,块留在缺陷记录里就成了指向不存在文件的坏链接,同时也在冒充仍然有效的
|
|
212
|
+
结论。目标环节之前已经完成的阶段结论保留不动。
|
|
213
|
+
"""
|
|
214
|
+
first_invalid = _RETURN_TARGET_FIRST_INVALID.get(target_stage)
|
|
215
|
+
if first_invalid is None:
|
|
216
|
+
return ""
|
|
217
|
+
invalid_labels = _RESULT_STAGE_LABELS[first_invalid:]
|
|
218
|
+
if not invalid_labels:
|
|
219
|
+
return ""
|
|
220
|
+
try:
|
|
221
|
+
records = _records_for_workflow(project_root, workflow_id, topics)
|
|
222
|
+
except ValueError:
|
|
223
|
+
# 本轮没有缺陷记录(非修缺陷轮次)时无需处理,不把它当成退回失败。
|
|
224
|
+
return ""
|
|
225
|
+
updated: list[str] = []
|
|
226
|
+
for path, _topic in records:
|
|
227
|
+
content = path.read_text(encoding="utf-8")
|
|
228
|
+
new_content = content
|
|
229
|
+
for label in invalid_labels:
|
|
230
|
+
new_content = re.sub(
|
|
231
|
+
rf"^###\s+{re.escape(label)}(工作流 {re.escape(workflow_id)})\s*$\n"
|
|
232
|
+
r".*?(?=^###\s+|\Z)",
|
|
233
|
+
"",
|
|
234
|
+
new_content,
|
|
235
|
+
flags=re.MULTILINE | re.DOTALL,
|
|
236
|
+
)
|
|
237
|
+
if new_content != content:
|
|
238
|
+
path.write_text(new_content.rstrip() + "\n", encoding="utf-8")
|
|
239
|
+
_update_index_status(project_root, path.name, "根因已确认")
|
|
240
|
+
updated.append(path.name)
|
|
241
|
+
if not updated:
|
|
242
|
+
return ""
|
|
243
|
+
return f"已清除失效的缺陷结论块并把索引状态改回“根因已确认”: {updated}"
|
|
244
|
+
|
|
245
|
+
|
|
139
246
|
def record_topic_acceptance_pass(project_root: str, workflow_id: str, topics: list[str]) -> str:
|
|
140
247
|
return update_status(
|
|
141
248
|
project_root,
|