@hunter-harness/workflow-harness 0.2.77 → 0.2.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +55 -0
- package/harness/bundles/general/claude-code/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/general/claude-code/scripts/harness_context.py +4 -0
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +102 -3
- package/harness/bundles/general/claude-code/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +4 -0
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +76 -2
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +55 -0
- package/harness/bundles/general/codebuddy/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +4 -0
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +102 -3
- package/harness/bundles/general/codebuddy/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +4 -0
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +76 -2
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +55 -0
- package/harness/bundles/general/codex/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/general/codex/scripts/harness_context.py +4 -0
- package/harness/bundles/general/codex/scripts/harness_fixback.py +102 -3
- package/harness/bundles/general/codex/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +4 -0
- package/harness/bundles/general/codex/scripts/harness_runtime.py +76 -2
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +55 -0
- package/harness/bundles/general/cursor/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/general/cursor/scripts/harness_context.py +4 -0
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +102 -3
- package/harness/bundles/general/cursor/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +4 -0
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +76 -2
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +55 -0
- package/harness/bundles/java/claude-code/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/java/claude-code/scripts/harness_context.py +4 -0
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +102 -3
- package/harness/bundles/java/claude-code/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +4 -0
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +76 -2
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +55 -0
- package/harness/bundles/java/codebuddy/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +4 -0
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +102 -3
- package/harness/bundles/java/codebuddy/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +4 -0
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +76 -2
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +55 -0
- package/harness/bundles/java/codex/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/java/codex/scripts/harness_context.py +4 -0
- package/harness/bundles/java/codex/scripts/harness_fixback.py +102 -3
- package/harness/bundles/java/codex/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +4 -0
- package/harness/bundles/java/codex/scripts/harness_runtime.py +76 -2
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/powershell-protocol.md +20 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +55 -0
- package/harness/bundles/java/cursor/scripts/harness_check_gate.py +2 -0
- package/harness/bundles/java/cursor/scripts/harness_context.py +4 -0
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +102 -3
- package/harness/bundles/java/cursor/scripts/harness_knowledge_candidates.py +262 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +4 -0
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +76 -2
- package/harness/manifests/general/claude-code.json +26 -22
- package/harness/manifests/general/codebuddy.json +26 -22
- package/harness/manifests/general/codex.json +26 -22
- package/harness/manifests/general/cursor.json +26 -22
- package/harness/manifests/java/claude-code.json +28 -24
- package/harness/manifests/java/codebuddy.json +28 -24
- package/harness/manifests/java/codex.json +28 -24
- package/harness/manifests/java/cursor.json +28 -24
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-archive — 变更归档
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-codebase-map — 代码库地图
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -8,7 +8,7 @@ disallowed-tools:
|
|
|
8
8
|
- Bash(git *)
|
|
9
9
|
- Bash(curl *)
|
|
10
10
|
---
|
|
11
|
-
<!-- generated by harness_deploy.py; core=
|
|
11
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
12
12
|
# harness-knowledge-ingest
|
|
13
13
|
|
|
14
14
|
知识 ingest 完全由 Hunter Platform 负责。归档完成时,客户端生成一个确定性 ZIP;
|
|
@@ -8,7 +8,7 @@ disallowed-tools:
|
|
|
8
8
|
- Bash(git *)
|
|
9
9
|
- Bash(curl *)
|
|
10
10
|
---
|
|
11
|
-
<!-- generated by harness_deploy.py; core=
|
|
11
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
12
12
|
# harness-knowledge-query
|
|
13
13
|
|
|
14
14
|
项目知识以 Hunter Platform 的服务端索引为唯一真源。客户端只提交查询并消费结果:
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-plan — 需求规划
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-pull — 从 Hunter Platform 下拉与恢复
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-push — 上传到 Hunter Platform
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -22,7 +22,7 @@ disallowed-tools:
|
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
- Bash(codegraph *)
|
|
24
24
|
---
|
|
25
|
-
<!-- generated by harness_deploy.py; core=
|
|
25
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
26
26
|
# harness-review — 代码审查
|
|
27
27
|
|
|
28
28
|
## Purpose
|
|
@@ -22,7 +22,7 @@ disallowed-tools:
|
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
- Bash(codegraph *)
|
|
24
24
|
---
|
|
25
|
-
<!-- generated by harness_deploy.py; core=
|
|
25
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
26
26
|
# harness-run — 需求编码
|
|
27
27
|
|
|
28
28
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-submit — 最终提交(含 worktree 合并)
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -19,7 +19,7 @@ disallowed-tools:
|
|
|
19
19
|
- Bash(awk *)
|
|
20
20
|
- Bash(curl *)
|
|
21
21
|
---
|
|
22
|
-
<!-- generated by harness_deploy.py; core=
|
|
22
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
23
23
|
# harness-sync
|
|
24
24
|
|
|
25
25
|
## Purpose
|
|
@@ -23,7 +23,7 @@ disallowed-tools:
|
|
|
23
23
|
- Bash(node *)
|
|
24
24
|
- Bash(codegraph *)
|
|
25
25
|
---
|
|
26
|
-
<!-- generated by harness_deploy.py; core=
|
|
26
|
+
<!-- generated by harness_deploy.py; core=5fc6a7ba2a0556ad; overlay=none; agent=claude-code; do not edit -->
|
|
27
27
|
# harness-test — 测试执行
|
|
28
28
|
|
|
29
29
|
## Purpose
|
|
@@ -196,6 +196,26 @@ Windows 默认常为 Windows PowerShell **5.1**。下列特性在 5.1 不可用
|
|
|
196
196
|
- 非 ASCII 路径(含中文)仓库:git/构建/文件操作一律 PowerShell;Bash 被 hook 拒绝(如 `Denied: non-ASCII path`)后**立即**改用 PowerShell 重发等价命令,禁止原样重试 Bash。
|
|
197
197
|
- Maven `-D` 参数在 PowerShell 中加引号:`"-Dmaven.test.skip=true"`。
|
|
198
198
|
|
|
199
|
+
### 捕获子进程中文输出:`$env:PYTHONUTF8=1` 不够
|
|
200
|
+
|
|
201
|
+
`PYTHONUTF8=1` 只让 **Python 按 UTF-8 输出**,不改变 **PowerShell 按什么编码读**。
|
|
202
|
+
控制台代码页是 GBK(936) 时,PowerShell 会用 GBK 解码 UTF-8 字节,得到形如
|
|
203
|
+
`绫?javadoc 鏈�闅� 6 绾х簿绠€` 的乱码——内容已在捕获阶段损坏,落进报告、回执与
|
|
204
|
+
JSON 字段后无法还原。
|
|
205
|
+
|
|
206
|
+
调用任何会输出中文的子进程前,**必须同时设置输出编码**:
|
|
207
|
+
|
|
208
|
+
```powershell
|
|
209
|
+
[Console]::OutputEncoding = [Text.Encoding]::UTF8
|
|
210
|
+
$env:PYTHONUTF8 = 1
|
|
211
|
+
python harness/scripts/harness_fixback.py <子命令> --json
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- 两行缺一不可:只设 `PYTHONUTF8` 会乱码,只设 `OutputEncoding` 则 Python 可能仍按本地编码输出。
|
|
215
|
+
- 写脚本时把这两行放在脚本开头,不要依赖调用方记得加。
|
|
216
|
+
- 少用 `2>&1` 合并流:PowerShell 5.1 会把 stderr 包装成 `NativeCommandError`,
|
|
217
|
+
与编码问题叠加后更难判断真实失败原因;需要 stderr 时分开捕获。
|
|
218
|
+
|
|
199
219
|
## 13. PS5/PS7 原生参数合同
|
|
200
220
|
|
|
201
221
|
复杂 JSON、Docker Go template、here-string 内容及多层引号不得作为 native process 的内联参数跨 `Bash → powershell -Command → native executable` 传递。统一写 UTF-8 JSON 参数文件:
|
|
@@ -91,6 +91,7 @@ import harness_events as he # noqa: E402
|
|
|
91
91
|
import harness_events_sync as hes # noqa: E402
|
|
92
92
|
import harness_efficiency as heff # noqa: E402
|
|
93
93
|
import harness_gate as hgate # noqa: E402
|
|
94
|
+
import harness_knowledge_candidates as hkc # noqa: E402
|
|
94
95
|
import harness_ledger as hl # noqa: E402
|
|
95
96
|
import harness_paths as hp # noqa: E402
|
|
96
97
|
import harness_phase as hphase # noqa: E402
|
|
@@ -4173,6 +4174,33 @@ def write_archive_meta(work_dir: Path, summary: dict[str, Any]) -> Path:
|
|
|
4173
4174
|
return out
|
|
4174
4175
|
|
|
4175
4176
|
|
|
4177
|
+
def write_knowledge_candidates(work_dir: Path, summary: dict[str, Any]) -> Path:
|
|
4178
|
+
"""Generate candidates/knowledge.json from summary-data (single ownership).
|
|
4179
|
+
|
|
4180
|
+
Mirrors write_archive_meta: derived from the same summary, written before the
|
|
4181
|
+
after-manifest so its bytes are covered. The archive directory name is the
|
|
4182
|
+
archive id, matching write_archive_meta's `archive-id` field.
|
|
4183
|
+
"""
|
|
4184
|
+
archive_id = work_dir.name
|
|
4185
|
+
candidates = hkc.build_knowledge_candidates(
|
|
4186
|
+
summary,
|
|
4187
|
+
change_key=str(summary.get("changeName") or archive_id),
|
|
4188
|
+
archive_id=archive_id,
|
|
4189
|
+
# The archive schema version identifies the producing format; there is
|
|
4190
|
+
# no separate harness version constant to borrow here.
|
|
4191
|
+
producer_version=SCHEMA_VERSION,
|
|
4192
|
+
created_at=now_iso(),
|
|
4193
|
+
)
|
|
4194
|
+
out = work_dir / "candidates" / "knowledge.json"
|
|
4195
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
4196
|
+
out.write_text(
|
|
4197
|
+
hkc.render_knowledge_candidates_json(candidates),
|
|
4198
|
+
encoding="utf-8",
|
|
4199
|
+
newline="\n",
|
|
4200
|
+
)
|
|
4201
|
+
return out
|
|
4202
|
+
|
|
4203
|
+
|
|
4176
4204
|
|
|
4177
4205
|
|
|
4178
4206
|
def _changed_files_from_git(
|
|
@@ -8269,6 +8297,21 @@ def cmd_finalize(
|
|
|
8269
8297
|
warnings.append(f"archive-meta write failed: {exc}")
|
|
8270
8298
|
payload["steps"]["archive_meta"] = {"ok": False, "error": str(exc)}
|
|
8271
8299
|
|
|
8300
|
+
# --- 8b. knowledge candidates (also before the after-manifest) ---
|
|
8301
|
+
# Soft-fail like archive-meta: an archive must never be rolled back because
|
|
8302
|
+
# knowledge extraction found nothing. An empty array is a valid outcome.
|
|
8303
|
+
try:
|
|
8304
|
+
summary = read_json(summary_path)
|
|
8305
|
+
candidates_path = write_knowledge_candidates(work_dir, summary)
|
|
8306
|
+
payload["steps"]["knowledge_candidates"] = {
|
|
8307
|
+
"ok": True,
|
|
8308
|
+
"path": str(candidates_path),
|
|
8309
|
+
"count": len(read_json(candidates_path)),
|
|
8310
|
+
}
|
|
8311
|
+
except Exception as exc: # noqa: BLE001 — candidates soft-fail
|
|
8312
|
+
warnings.append(f"knowledge candidates write failed: {exc}")
|
|
8313
|
+
payload["steps"]["knowledge_candidates"] = {"ok": False, "error": str(exc)}
|
|
8314
|
+
|
|
8272
8315
|
# --- 9/10. final summary stats, then LAST manifest (IA-7) ---
|
|
8273
8316
|
# Post-manifest rewrites of covered bytes are forbidden. We update the
|
|
8274
8317
|
# summary first, regenerate after-manifest last, then verify on-disk hashes.
|
|
@@ -8885,6 +8928,12 @@ def _archive_core_file_specs(
|
|
|
8885
8928
|
file_specs.append(
|
|
8886
8929
|
(source, f"{folder}/{relative}", role, "text/markdown")
|
|
8887
8930
|
)
|
|
8931
|
+
add_if_file(
|
|
8932
|
+
archive / "candidates" / "knowledge.json",
|
|
8933
|
+
"candidates/knowledge.json",
|
|
8934
|
+
"knowledge_candidates",
|
|
8935
|
+
"application/json",
|
|
8936
|
+
)
|
|
8888
8937
|
add_if_file(
|
|
8889
8938
|
archive / "meta" / "archive-meta.md",
|
|
8890
8939
|
"archive-meta.md",
|
|
@@ -8913,6 +8962,8 @@ def _archive_source_identity(project_root: Path) -> dict[str, str | None]:
|
|
|
8913
8962
|
["git", *args],
|
|
8914
8963
|
cwd=str(project_root),
|
|
8915
8964
|
text=True,
|
|
8965
|
+
encoding="utf-8",
|
|
8966
|
+
errors="replace",
|
|
8916
8967
|
stdout=subprocess.PIPE,
|
|
8917
8968
|
stderr=subprocess.DEVNULL,
|
|
8918
8969
|
timeout=10,
|
|
@@ -9449,6 +9500,10 @@ def auto_push_archive_core(
|
|
|
9449
9500
|
command,
|
|
9450
9501
|
cwd=str(project_root),
|
|
9451
9502
|
text=True,
|
|
9503
|
+
# 不指定 encoding 时中文 Windows 按 cp936 解码 UTF-8,
|
|
9504
|
+
# CLI 的中文回执会被损坏,随后的 json.loads 也可能失败。
|
|
9505
|
+
encoding="utf-8",
|
|
9506
|
+
errors="replace",
|
|
9452
9507
|
stdout=subprocess.PIPE,
|
|
9453
9508
|
stderr=subprocess.PIPE,
|
|
9454
9509
|
timeout=180,
|
|
@@ -184,6 +184,8 @@ def _head(project: Path) -> str | None:
|
|
|
184
184
|
process = subprocess.run(
|
|
185
185
|
["git", "-C", str(project), "rev-parse", "--verify", "HEAD"],
|
|
186
186
|
text=True,
|
|
187
|
+
encoding="utf-8",
|
|
188
|
+
errors="replace",
|
|
187
189
|
stdout=subprocess.PIPE,
|
|
188
190
|
stderr=subprocess.PIPE,
|
|
189
191
|
check=False,
|
|
@@ -196,6 +198,8 @@ def _git_common_dir(project: Path) -> Path | None:
|
|
|
196
198
|
process = subprocess.run(
|
|
197
199
|
["git", "-C", str(project), "rev-parse", "--git-common-dir"],
|
|
198
200
|
text=True,
|
|
201
|
+
encoding="utf-8",
|
|
202
|
+
errors="replace",
|
|
199
203
|
stdout=subprocess.PIPE,
|
|
200
204
|
stderr=subprocess.PIPE,
|
|
201
205
|
check=False,
|
|
@@ -587,6 +587,13 @@ def launch_review_fixback(
|
|
|
587
587
|
"transition": begun,
|
|
588
588
|
"gate": gate,
|
|
589
589
|
"batch": batch,
|
|
590
|
+
# 证据要求随开批次一起交底:晚一步暴露,调用方就已经改完代码,
|
|
591
|
+
# 只能回退伪造 RED。
|
|
592
|
+
"evidenceContract": evidence_contract(
|
|
593
|
+
change_dir=str(change_dir),
|
|
594
|
+
batch_id=str(batch.get("batchId") or ""),
|
|
595
|
+
product_identity=str(batch.get("productIdentity") or ""),
|
|
596
|
+
),
|
|
590
597
|
}
|
|
591
598
|
|
|
592
599
|
|
|
@@ -802,12 +809,102 @@ def _resolve_evidence_path(change_dir: Path, raw_path: str) -> Path:
|
|
|
802
809
|
)
|
|
803
810
|
path = next((candidate for candidate in candidates if candidate.is_file()), None)
|
|
804
811
|
if path is None:
|
|
805
|
-
raise ValueError(
|
|
812
|
+
raise ValueError(evidence_error_message("FIXBACK_EVIDENCE_MISSING", raw_path))
|
|
806
813
|
if not _is_within(path, [contract_root, state_root, project_root]):
|
|
807
|
-
raise ValueError(
|
|
814
|
+
raise ValueError(
|
|
815
|
+
evidence_error_message("FIXBACK_EVIDENCE_OUTSIDE_PROJECT", raw_path)
|
|
816
|
+
)
|
|
808
817
|
return path
|
|
809
818
|
|
|
810
819
|
|
|
820
|
+
def evidence_contract(
|
|
821
|
+
*,
|
|
822
|
+
change_dir: str | None = None,
|
|
823
|
+
batch_id: str | None = None,
|
|
824
|
+
product_identity: str | None = None,
|
|
825
|
+
) -> dict[str, Any]:
|
|
826
|
+
"""开批次时就交出证据契约,而不是等 resolve-issue 报错才逼调用方逆向。
|
|
827
|
+
|
|
828
|
+
契约本身没变过:每个 code 项需要一条修复前的 RED(会话 FAIL)与一条修复后的
|
|
829
|
+
GREEN(会话 OK),两者都必须是 run-start 产出的托管会话证据,且先注册后引用。
|
|
830
|
+
问题在于它此前只写在实现里——调用方通常已经改完代码才发现 RED 要"修复前",
|
|
831
|
+
于是把改动回退、跑一次假 RED、再改回来,凭空多出一轮返工与一段脏工作树。
|
|
832
|
+
"""
|
|
833
|
+
change = change_dir or "<change-dir>"
|
|
834
|
+
batch = batch_id or "<batch-id>"
|
|
835
|
+
product = product_identity or "<product-identity>"
|
|
836
|
+
return {
|
|
837
|
+
"why": "RED 证明问题真实存在,GREEN 证明修复真的生效;缺任一条都无法关批次。",
|
|
838
|
+
"order": [
|
|
839
|
+
"1. 修复前:先用 run-start 跑一次能复现该问题的命令,得到 status=FAIL 的会话(RED)",
|
|
840
|
+
"2. 实施修复",
|
|
841
|
+
"3. 修复后:用同一条命令再跑一次 run-start,得到 status=OK 的会话(GREEN)",
|
|
842
|
+
"4. 两条证据各写一个 JSON 文件,分别 register-evidence 注册",
|
|
843
|
+
"5. resolve-issue 引用这两个已注册的证据文件",
|
|
844
|
+
],
|
|
845
|
+
"antiPattern": (
|
|
846
|
+
"不要先改完再把修改回退来凑 RED:那样 RED 证明的只是回退后的状态,"
|
|
847
|
+
"不是原始缺陷,中途还会留下脏工作树。"
|
|
848
|
+
),
|
|
849
|
+
"commands": {
|
|
850
|
+
"collectSession": (
|
|
851
|
+
"python harness_runtime.py run-start --state-root "
|
|
852
|
+
f"{change} --verification <名称> --working-directory . "
|
|
853
|
+
f"--product-identity {product} --json -- <复现/验证命令>"
|
|
854
|
+
),
|
|
855
|
+
"awaitSession": (
|
|
856
|
+
"python harness_runtime.py run-status --state-root "
|
|
857
|
+
f"{change} --session-id <sessionId> --wait --json"
|
|
858
|
+
),
|
|
859
|
+
"register": (
|
|
860
|
+
"python harness_fixback.py register-evidence --change-dir "
|
|
861
|
+
f"{change} --evidence <证据 JSON 路径>"
|
|
862
|
+
),
|
|
863
|
+
"resolve": (
|
|
864
|
+
"python harness_fixback.py resolve-issue --change-dir "
|
|
865
|
+
f"{change} --batch-id {batch} --issue-id <issueId> "
|
|
866
|
+
"--red-evidence <red.json> --green-evidence <green.json>"
|
|
867
|
+
),
|
|
868
|
+
},
|
|
869
|
+
"evidenceFile": {
|
|
870
|
+
"schemaVersion": 2,
|
|
871
|
+
"kind": ["red", "green"],
|
|
872
|
+
"status": {"red": "FAIL", "green": "OK"},
|
|
873
|
+
"provenance": {
|
|
874
|
+
"type": "managed-run-session",
|
|
875
|
+
"note": "指向 run-start 产出的 runtime/run-sessions/<sessionId>/session.json",
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
_EVIDENCE_ERROR_HINTS = {
|
|
882
|
+
"FIXBACK_EVIDENCE_MISSING": (
|
|
883
|
+
"路径未找到证据文件。它应当是 run-start 产出的托管会话证据 JSON"
|
|
884
|
+
"(schemaVersion=2,kind=red|green,provenance.type=managed-run-session);"
|
|
885
|
+
"先用 run-start 采集会话,再据此写证据文件。"
|
|
886
|
+
),
|
|
887
|
+
"FIXBACK_EVIDENCE_UNREGISTERED": (
|
|
888
|
+
"证据文件存在但未注册,或注册后内容已变。"
|
|
889
|
+
"先执行 register-evidence 注册(内容改动后需重新注册),再引用。"
|
|
890
|
+
),
|
|
891
|
+
"FIXBACK_EVIDENCE_INVALID": (
|
|
892
|
+
"证据文件不是合法的 schemaVersion=2 托管会话证据。"
|
|
893
|
+
"检查 kind / status / provenance 三个字段。"
|
|
894
|
+
),
|
|
895
|
+
"FIXBACK_EVIDENCE_OUTSIDE_PROJECT": (
|
|
896
|
+
"证据路径落在项目之外。证据必须放在变更目录或项目内,便于随归档留痕。"
|
|
897
|
+
),
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def evidence_error_message(code: str, raw_path: str) -> str:
|
|
902
|
+
"""把只回显路径的错误码,补成能直接照做的一句话。"""
|
|
903
|
+
hint = _EVIDENCE_ERROR_HINTS.get(code)
|
|
904
|
+
base = f"{code}: {raw_path}"
|
|
905
|
+
return base if hint is None else f"{base} — {hint}"
|
|
906
|
+
|
|
907
|
+
|
|
811
908
|
def register_evidence(change_dir: Path, raw_path: str) -> dict[str, Any]:
|
|
812
909
|
"""Register evidence only after validating its authoritative provenance."""
|
|
813
910
|
path = _resolve_evidence_path(change_dir, raw_path)
|
|
@@ -968,7 +1065,9 @@ def _evidence_record(
|
|
|
968
1065
|
or ledger_record.get("productIdentity") != evidence_product
|
|
969
1066
|
or ledger_record.get("passedGates") != sorted(set(passed_gates))
|
|
970
1067
|
):
|
|
971
|
-
raise ValueError(
|
|
1068
|
+
raise ValueError(
|
|
1069
|
+
evidence_error_message("FIXBACK_EVIDENCE_UNREGISTERED", raw_path)
|
|
1070
|
+
)
|
|
972
1071
|
if product_identity is not None and evidence_product != product_identity:
|
|
973
1072
|
raise ValueError(
|
|
974
1073
|
"FIXBACK_EVIDENCE_IDENTITY_MISMATCH: "
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Knowledge candidate generation from an archived change's summary-data.json.
|
|
3
|
+
|
|
4
|
+
The archive workflow has already filtered once: review findings were produced by
|
|
5
|
+
an independent reviewer and then adjudicated, and knownRisks are evidence-derived
|
|
6
|
+
facts. This module turns those two — and only those two — into KnowledgeCandidate
|
|
7
|
+
records for the archive package's ``candidates/knowledge.json``.
|
|
8
|
+
|
|
9
|
+
Mapping is fixed by docs/superpowers/specs/2026-08-18-three-views-data-flow-design.md
|
|
10
|
+
("知识来源的选定")::
|
|
11
|
+
|
|
12
|
+
disposition = FIXED -> pitfall RED 0.95 / YELLOW 0.85
|
|
13
|
+
disposition = ACCEPTED_RISK | DEFERRED -> risk RED 0.95 / YELLOW 0.85
|
|
14
|
+
knownRisks[] -> risk 0.85
|
|
15
|
+
severity = OK | disposition = NOT_APPLICABLE -> dropped
|
|
16
|
+
|
|
17
|
+
Dispositions outside the adopted set (OPEN / UNKNOWN) are dropped too: an
|
|
18
|
+
unadjudicated finding is not yet knowledge. maintenanceNotes, finalStatusReasons
|
|
19
|
+
and manualActions are deliberately excluded — the spec evaluated each and found
|
|
20
|
+
them too noisy or empty to be worth persisting.
|
|
21
|
+
|
|
22
|
+
No LLM is involved. Every emitted field is copied or derived from a real
|
|
23
|
+
summary-data field, so the output is reproducible and free of invention.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import json
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
SCHEMA_VERSION = 1
|
|
32
|
+
PRODUCER = "harness-archive"
|
|
33
|
+
|
|
34
|
+
# Only these severities carry knowledge; OK is explicitly dropped by the spec.
|
|
35
|
+
_SEVERITIES = {"RED", "YELLOW"}
|
|
36
|
+
# Adjudicated dispositions the spec adopts, mapped to the knowledge entry type.
|
|
37
|
+
_DISPOSITION_ENTRY_TYPES = {
|
|
38
|
+
"FIXED": "pitfall",
|
|
39
|
+
"ACCEPTED_RISK": "risk",
|
|
40
|
+
"DEFERRED": "risk",
|
|
41
|
+
}
|
|
42
|
+
_SEVERITY_CONFIDENCE = {"RED": 0.95, "YELLOW": 0.85}
|
|
43
|
+
_KNOWN_RISK_CONFIDENCE = 0.85
|
|
44
|
+
|
|
45
|
+
_MAX_KEYWORDS = 32
|
|
46
|
+
_MAX_KEYWORD_CHARS = 80
|
|
47
|
+
_MAX_BODY_CHARS = 20_000
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _text(value: Any) -> str:
|
|
51
|
+
return value.strip() if isinstance(value, str) else ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _digest(*parts: str) -> str:
|
|
55
|
+
return hashlib.sha256("\0".join(parts).encode("utf-8")).hexdigest()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _candidate_id(change_key: str, kind: str, identity: str) -> str:
|
|
59
|
+
return f"kc_{_digest(change_key, kind, identity)[:32]}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _content_hash(entry_type: str, summary: str, body: str, keywords: list[str]) -> str:
|
|
63
|
+
canonical = json.dumps(
|
|
64
|
+
{"entry_type": entry_type, "summary": summary, "body": body, "keywords": keywords},
|
|
65
|
+
ensure_ascii=False,
|
|
66
|
+
sort_keys=True,
|
|
67
|
+
separators=(",", ":"),
|
|
68
|
+
)
|
|
69
|
+
return "sha256:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _keywords(*values: str) -> list[str]:
|
|
73
|
+
"""Deduplicate, preserve order, and honour the contract's bounds."""
|
|
74
|
+
seen: list[str] = []
|
|
75
|
+
for value in values:
|
|
76
|
+
keyword = _text(value)[:_MAX_KEYWORD_CHARS]
|
|
77
|
+
if keyword and keyword not in seen:
|
|
78
|
+
seen.append(keyword)
|
|
79
|
+
return seen[:_MAX_KEYWORDS]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _path_segments(path: str) -> list[str]:
|
|
83
|
+
return [segment for segment in path.replace("\\", "/").split("/") if segment]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _location(path: str, line: Any) -> str:
|
|
87
|
+
"""``path:line`` when the line number is real, otherwise just the path."""
|
|
88
|
+
if not path:
|
|
89
|
+
return ""
|
|
90
|
+
if isinstance(line, bool) or not isinstance(line, int) or line < 1:
|
|
91
|
+
return path
|
|
92
|
+
return f"{path}:{line}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _finding_candidate(
|
|
96
|
+
finding: dict[str, Any],
|
|
97
|
+
*,
|
|
98
|
+
change_key: str,
|
|
99
|
+
archive_id: str,
|
|
100
|
+
producer_version: str,
|
|
101
|
+
created_at: str,
|
|
102
|
+
) -> dict[str, Any] | None:
|
|
103
|
+
severity = _text(finding.get("severity"))
|
|
104
|
+
disposition = _text(finding.get("disposition"))
|
|
105
|
+
entry_type = _DISPOSITION_ENTRY_TYPES.get(disposition)
|
|
106
|
+
title = _text(finding.get("title"))
|
|
107
|
+
if severity not in _SEVERITIES or entry_type is None or not title:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
path = _text(finding.get("path"))
|
|
111
|
+
line = finding.get("line")
|
|
112
|
+
location = _location(path, line)
|
|
113
|
+
segments = _path_segments(path)
|
|
114
|
+
finding_id = _text(finding.get("id"))
|
|
115
|
+
|
|
116
|
+
body_lines = [title]
|
|
117
|
+
if location:
|
|
118
|
+
body_lines.append(f"位置:{location}")
|
|
119
|
+
body_lines.append(f"严重度:{severity}")
|
|
120
|
+
body_lines.append(f"裁决:{disposition}")
|
|
121
|
+
body = "\n".join(body_lines)[:_MAX_BODY_CHARS]
|
|
122
|
+
|
|
123
|
+
keywords = _keywords(
|
|
124
|
+
segments[-1] if segments else "",
|
|
125
|
+
segments[-2] if len(segments) >= 2 else "",
|
|
126
|
+
severity,
|
|
127
|
+
disposition,
|
|
128
|
+
)
|
|
129
|
+
source_ref = f"archive:{archive_id}#{finding_id}" if finding_id else f"archive:{archive_id}"
|
|
130
|
+
if path and location != path:
|
|
131
|
+
source_refs = [f"{path}#L{line}"]
|
|
132
|
+
elif path:
|
|
133
|
+
source_refs = [path]
|
|
134
|
+
else:
|
|
135
|
+
source_refs = [f"archive:{archive_id}"]
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"schema_version": SCHEMA_VERSION,
|
|
139
|
+
"candidate_id": _candidate_id(
|
|
140
|
+
change_key, "review", finding_id or f"{title}\0{path}\0{line}"
|
|
141
|
+
),
|
|
142
|
+
"source_change_key": change_key,
|
|
143
|
+
"source_refs": source_refs,
|
|
144
|
+
"summary": title,
|
|
145
|
+
"reusability_scope": segments[0] if segments else "project",
|
|
146
|
+
"content_hash": _content_hash(entry_type, title, body, keywords),
|
|
147
|
+
"confidence": _SEVERITY_CONFIDENCE[severity],
|
|
148
|
+
"status": "pending",
|
|
149
|
+
"entry_type": entry_type,
|
|
150
|
+
"body": body,
|
|
151
|
+
"keywords": keywords,
|
|
152
|
+
"provenance": {
|
|
153
|
+
"source_kind": "review",
|
|
154
|
+
"source_ref": source_ref,
|
|
155
|
+
"producer": PRODUCER,
|
|
156
|
+
"producer_version": producer_version,
|
|
157
|
+
"created_at": created_at,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _risk_candidate(
|
|
163
|
+
risk: dict[str, Any],
|
|
164
|
+
*,
|
|
165
|
+
change_key: str,
|
|
166
|
+
archive_id: str,
|
|
167
|
+
producer_version: str,
|
|
168
|
+
created_at: str,
|
|
169
|
+
) -> dict[str, Any] | None:
|
|
170
|
+
message = _text(risk.get("message"))
|
|
171
|
+
if not message:
|
|
172
|
+
return None
|
|
173
|
+
phase = _text(risk.get("phase"))
|
|
174
|
+
severity = _text(risk.get("severity"))
|
|
175
|
+
|
|
176
|
+
body_lines = [message]
|
|
177
|
+
if phase:
|
|
178
|
+
body_lines.append(f"阶段:{phase}")
|
|
179
|
+
if severity:
|
|
180
|
+
body_lines.append(f"严重度:{severity}")
|
|
181
|
+
body = "\n".join(body_lines)[:_MAX_BODY_CHARS]
|
|
182
|
+
keywords = _keywords(phase, severity)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"schema_version": SCHEMA_VERSION,
|
|
186
|
+
"candidate_id": _candidate_id(change_key, "known_risk", f"{phase}\0{message}"),
|
|
187
|
+
"source_change_key": change_key,
|
|
188
|
+
"source_refs": [f"archive:{archive_id}"],
|
|
189
|
+
"summary": message,
|
|
190
|
+
"reusability_scope": phase or "project",
|
|
191
|
+
"content_hash": _content_hash("risk", message, body, keywords),
|
|
192
|
+
"confidence": _KNOWN_RISK_CONFIDENCE,
|
|
193
|
+
"status": "pending",
|
|
194
|
+
"entry_type": "risk",
|
|
195
|
+
"body": body,
|
|
196
|
+
"keywords": keywords,
|
|
197
|
+
"provenance": {
|
|
198
|
+
"source_kind": "archive",
|
|
199
|
+
"source_ref": f"archive:{archive_id}",
|
|
200
|
+
"producer": PRODUCER,
|
|
201
|
+
"producer_version": producer_version,
|
|
202
|
+
"created_at": created_at,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def build_knowledge_candidates(
|
|
208
|
+
summary: dict[str, Any],
|
|
209
|
+
*,
|
|
210
|
+
change_key: str,
|
|
211
|
+
archive_id: str,
|
|
212
|
+
producer_version: str,
|
|
213
|
+
created_at: str,
|
|
214
|
+
) -> list[dict[str, Any]]:
|
|
215
|
+
"""Project reviewFindings + knownRisks into KnowledgeCandidate records.
|
|
216
|
+
|
|
217
|
+
Returns [] for missing or malformed input: an archive with nothing worth
|
|
218
|
+
persisting must still produce a valid (empty) candidates file.
|
|
219
|
+
"""
|
|
220
|
+
if not isinstance(summary, dict):
|
|
221
|
+
return []
|
|
222
|
+
candidates: list[dict[str, Any]] = []
|
|
223
|
+
seen_ids: set[str] = set()
|
|
224
|
+
|
|
225
|
+
def collect(candidate: dict[str, Any] | None) -> None:
|
|
226
|
+
if candidate is None or candidate["candidate_id"] in seen_ids:
|
|
227
|
+
return
|
|
228
|
+
seen_ids.add(candidate["candidate_id"])
|
|
229
|
+
candidates.append(candidate)
|
|
230
|
+
|
|
231
|
+
findings = summary.get("reviewFindings")
|
|
232
|
+
if isinstance(findings, list):
|
|
233
|
+
for finding in findings:
|
|
234
|
+
if isinstance(finding, dict):
|
|
235
|
+
collect(_finding_candidate(
|
|
236
|
+
finding,
|
|
237
|
+
change_key=change_key,
|
|
238
|
+
archive_id=archive_id,
|
|
239
|
+
producer_version=producer_version,
|
|
240
|
+
created_at=created_at,
|
|
241
|
+
))
|
|
242
|
+
|
|
243
|
+
risks = summary.get("knownRisks")
|
|
244
|
+
if isinstance(risks, list):
|
|
245
|
+
for risk in risks:
|
|
246
|
+
if isinstance(risk, dict):
|
|
247
|
+
collect(_risk_candidate(
|
|
248
|
+
risk,
|
|
249
|
+
change_key=change_key,
|
|
250
|
+
archive_id=archive_id,
|
|
251
|
+
producer_version=producer_version,
|
|
252
|
+
created_at=created_at,
|
|
253
|
+
))
|
|
254
|
+
|
|
255
|
+
return candidates
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def render_knowledge_candidates_json(candidates: list[dict[str, Any]]) -> str:
|
|
259
|
+
"""Deterministic bytes for the archive package entry."""
|
|
260
|
+
return json.dumps(
|
|
261
|
+
candidates, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
262
|
+
) + "\n"
|