@fitlab-ai/agent-infra 0.11.15 → 0.12.1

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.
Files changed (265) hide show
  1. package/bin/cli.ts +1 -1
  2. package/dist/bin/cli.js +1 -1
  3. package/dist/bin/internal-cli.js +6 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +6 -6
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +8 -18
  6. package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -1
  7. package/dist/lib/decide.js +2 -7
  8. package/dist/lib/internal/cli-route-inventory.js +3 -0
  9. package/dist/lib/internal/manual-validation.js +19 -33
  10. package/dist/lib/internal/platform-comment.js +59 -9
  11. package/dist/lib/internal/task-activity.js +1 -12
  12. package/dist/lib/internal/task-event.js +1 -12
  13. package/dist/lib/internal/task-ledger.js +1 -12
  14. package/dist/lib/internal/task-operation-registry.js +2 -1
  15. package/dist/lib/internal/task-qualification.js +6 -15
  16. package/dist/lib/internal/task-review.js +3 -16
  17. package/dist/lib/internal/task-short-id.js +2 -1
  18. package/dist/lib/internal/task-verify.js +1 -11
  19. package/dist/lib/internal/task-warning.js +1 -12
  20. package/dist/lib/internal/task-workspace.js +43 -0
  21. package/dist/lib/merge.js +4 -3
  22. package/dist/lib/platform/issue-comments.js +373 -267
  23. package/dist/lib/platform/issue-summary-content.js +112 -0
  24. package/dist/lib/platform/pr-change-report.js +175 -26
  25. package/dist/lib/platform/pr-summary-content.js +54 -0
  26. package/dist/lib/platform/pr-summary.js +321 -296
  27. package/dist/lib/platform/pull-requests.js +3 -15
  28. package/dist/lib/platform/summary-content-pipeline.js +18 -0
  29. package/dist/lib/pr-review/evidence-grading.js +3 -2
  30. package/dist/lib/process-data/index.js +5 -4
  31. package/dist/lib/process-data/sources.js +6 -0
  32. package/dist/lib/run/index.js +3 -2
  33. package/dist/lib/sandbox/commands/create.js +5 -1
  34. package/dist/lib/sandbox/commands/enter.js +1 -1
  35. package/dist/lib/sandbox/commands/list-running.js +2 -1
  36. package/dist/lib/sandbox/control/client.js +8 -13
  37. package/dist/lib/sandbox/control/completed-reentry.js +2 -1
  38. package/dist/lib/sandbox/control/task-view.js +4 -2
  39. package/dist/lib/sandbox/mounts.js +4 -4
  40. package/dist/lib/sandbox/recovery.js +26 -16
  41. package/dist/lib/sandbox/removal.js +62 -12
  42. package/dist/lib/sandbox/task-resolver.js +9 -6
  43. package/dist/lib/sandbox/workspace-identity.js +6 -5
  44. package/dist/lib/sandbox/workspace-view.js +29 -22
  45. package/dist/lib/task/activity-intent.js +2 -12
  46. package/dist/lib/task/artifact-lifecycle.js +17 -11
  47. package/dist/lib/task/artifact-operations.js +41 -49
  48. package/dist/lib/task/capabilities.js +2 -2
  49. package/dist/lib/task/commands/ls.js +4 -3
  50. package/dist/lib/task/commands/status.js +1 -1
  51. package/dist/lib/task/commit-operation.js +6 -6
  52. package/dist/lib/task/control-authority.js +32 -32
  53. package/dist/lib/task/create-service.js +2 -4
  54. package/dist/lib/task/create.js +12 -65
  55. package/dist/lib/task/delegation-receipts.js +1 -1
  56. package/dist/lib/task/delivery.js +2 -8
  57. package/dist/lib/task/events.js +5 -31
  58. package/dist/lib/task/finalization.js +33 -29
  59. package/dist/lib/task/lifecycle.js +9 -48
  60. package/dist/lib/task/local-artifact-finalization.js +1 -2
  61. package/dist/lib/task/orchestration.js +24 -57
  62. package/dist/lib/task/platform-operation-recovery.js +87 -88
  63. package/dist/lib/task/resolve-ref.js +17 -12
  64. package/dist/lib/task/review-finalization.js +2 -35
  65. package/dist/lib/task/review-identity.js +3 -1
  66. package/dist/lib/task/short-id.js +10 -8
  67. package/dist/lib/task/verification-engine.js +19 -58
  68. package/dist/lib/task/workspace-root.js +59 -0
  69. package/dist/lib/task/workspace-snapshot.js +4 -3
  70. package/dist/lifecycle-build-manifest.json +3 -3
  71. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +6 -6
  72. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +8 -34
  73. package/lib/agent-clients/adapters/codex-orchestration.ts +1 -1
  74. package/lib/decide.ts +2 -7
  75. package/lib/internal/cli-route-inventory.ts +2 -0
  76. package/lib/internal/manual-validation.ts +20 -37
  77. package/lib/internal/platform-comment.ts +49 -6
  78. package/lib/internal/task-activity.ts +1 -10
  79. package/lib/internal/task-event.ts +1 -10
  80. package/lib/internal/task-ledger.ts +1 -10
  81. package/lib/internal/task-operation-registry.ts +2 -1
  82. package/lib/internal/task-qualification.ts +5 -11
  83. package/lib/internal/task-review.ts +3 -14
  84. package/lib/internal/task-short-id.ts +2 -1
  85. package/lib/internal/task-verify.ts +1 -9
  86. package/lib/internal/task-warning.ts +1 -10
  87. package/lib/internal/task-workspace.ts +39 -0
  88. package/lib/merge.ts +4 -3
  89. package/lib/platform/issue-comments.ts +478 -280
  90. package/lib/platform/issue-summary-content.ts +132 -0
  91. package/lib/platform/pr-change-report.ts +165 -20
  92. package/lib/platform/pr-summary-content.ts +60 -0
  93. package/lib/platform/pr-summary.ts +367 -269
  94. package/lib/platform/pull-requests.ts +3 -24
  95. package/lib/platform/summary-content-pipeline.ts +23 -0
  96. package/lib/pr-review/evidence-grading.ts +3 -2
  97. package/lib/process-data/index.ts +4 -3
  98. package/lib/process-data/sources.ts +6 -0
  99. package/lib/run/index.ts +3 -2
  100. package/lib/sandbox/commands/create.ts +5 -1
  101. package/lib/sandbox/commands/enter.ts +1 -1
  102. package/lib/sandbox/commands/list-running.ts +2 -1
  103. package/lib/sandbox/control/client.ts +7 -12
  104. package/lib/sandbox/control/completed-reentry.ts +2 -1
  105. package/lib/sandbox/control/task-view.ts +4 -2
  106. package/lib/sandbox/mounts.ts +4 -4
  107. package/lib/sandbox/recovery.ts +26 -18
  108. package/lib/sandbox/removal.ts +60 -27
  109. package/lib/sandbox/task-resolver.ts +9 -6
  110. package/lib/sandbox/workspace-identity.ts +6 -5
  111. package/lib/sandbox/workspace-view.ts +28 -20
  112. package/lib/task/activity-intent.ts +3 -17
  113. package/lib/task/artifact-lifecycle.ts +39 -11
  114. package/lib/task/artifact-operations.ts +28 -35
  115. package/lib/task/capabilities.ts +2 -2
  116. package/lib/task/commands/ls.ts +4 -3
  117. package/lib/task/commands/status.ts +1 -1
  118. package/lib/task/commit-operation.ts +5 -6
  119. package/lib/task/control-authority.ts +29 -35
  120. package/lib/task/create-service.ts +2 -4
  121. package/lib/task/create.ts +10 -51
  122. package/lib/task/delegation-receipts.ts +1 -1
  123. package/lib/task/delivery.ts +2 -10
  124. package/lib/task/events.ts +5 -32
  125. package/lib/task/finalization.ts +27 -30
  126. package/lib/task/lifecycle.ts +9 -50
  127. package/lib/task/local-artifact-finalization.ts +1 -4
  128. package/lib/task/orchestration.ts +31 -80
  129. package/lib/task/platform-operation-recovery.ts +70 -82
  130. package/lib/task/resolve-ref.ts +18 -12
  131. package/lib/task/review-finalization.ts +2 -52
  132. package/lib/task/review-identity.ts +3 -1
  133. package/lib/task/short-id.ts +10 -8
  134. package/lib/task/verification-engine.ts +20 -62
  135. package/lib/task/workspace-root.ts +78 -0
  136. package/lib/task/workspace-snapshot.ts +4 -3
  137. package/package.json +1 -1
  138. package/templates/.agents/QUICKSTART.en.md +19 -8
  139. package/templates/.agents/QUICKSTART.zh-CN.md +19 -8
  140. package/templates/.agents/README.en.md +10 -15
  141. package/templates/.agents/README.zh-CN.md +9 -13
  142. package/templates/.agents/rules/issue-sync.en.md +1 -1
  143. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  144. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  145. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  146. package/templates/.agents/rules/next-step-output.en.md +2 -2
  147. package/templates/.agents/rules/next-step-output.zh-CN.md +2 -2
  148. package/templates/.agents/rules/pr-sync.en.md +5 -3
  149. package/templates/.agents/rules/pr-sync.zh-CN.md +7 -10
  150. package/templates/.agents/rules/release-commands.en.md +3 -0
  151. package/templates/.agents/rules/release-commands.zh-CN.md +3 -0
  152. package/templates/.agents/rules/task-management.en.md +0 -2
  153. package/templates/.agents/rules/task-management.zh-CN.md +0 -2
  154. package/templates/.agents/rules/task-short-id.en.md +2 -2
  155. package/templates/.agents/rules/task-short-id.zh-CN.md +2 -2
  156. package/templates/.agents/scripts/task-short-id.js +2 -8
  157. package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
  158. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
  159. package/templates/.agents/skills/analyze-task/config/verify.en.json +0 -1
  160. package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +0 -1
  161. package/templates/.agents/skills/archive-tasks/SKILL.en.md +5 -5
  162. package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +5 -5
  163. package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +10 -2
  164. package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
  165. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +5 -5
  166. package/templates/.agents/skills/block-task/config/verify.json +0 -1
  167. package/templates/.agents/skills/cancel-task/SKILL.en.md +7 -7
  168. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +7 -7
  169. package/templates/.agents/skills/cancel-task/config/verify.json +0 -1
  170. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  171. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
  172. package/templates/.agents/skills/code-task/config/verify.en.json +0 -1
  173. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +0 -1
  174. package/templates/.agents/skills/code-task/reference/output-template.en.md +1 -1
  175. package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
  176. package/templates/.agents/skills/commit/SKILL.en.md +1 -1
  177. package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
  178. package/templates/.agents/skills/commit/config/verify.json +0 -1
  179. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +2 -2
  180. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +2 -2
  181. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +2 -2
  182. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +2 -2
  183. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
  184. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
  185. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +0 -1
  186. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +0 -1
  187. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +3 -3
  188. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +3 -3
  189. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -30
  190. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +17 -32
  191. package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
  192. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
  193. package/templates/.agents/skills/create-pr/SKILL.en.md +3 -3
  194. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -3
  195. package/templates/.agents/skills/create-pr/config/verify.json +0 -1
  196. package/templates/.agents/skills/create-pr/reference/change-report.en.md +10 -3
  197. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +10 -3
  198. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +3 -3
  199. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +3 -3
  200. package/templates/.agents/skills/create-release-note/SKILL.en.md +2 -2
  201. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +2 -2
  202. package/templates/.agents/skills/create-task/SKILL.en.md +4 -22
  203. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -22
  204. package/templates/.agents/skills/create-task/config/verify.json +0 -1
  205. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  206. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  207. package/templates/.agents/skills/import-codescan/config/verify.json +0 -1
  208. package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -3
  209. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
  210. package/templates/.agents/skills/import-dependabot/config/verify.json +0 -1
  211. package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
  212. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
  213. package/templates/.agents/skills/import-issue/config/verify.json +0 -1
  214. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
  215. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
  216. package/templates/.agents/skills/plan-task/config/verify.en.json +0 -1
  217. package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +0 -1
  218. package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
  219. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -3
  220. package/templates/.agents/skills/restore-task/config/verify.json +0 -1
  221. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  222. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
  223. package/templates/.agents/skills/review-analysis/config/verify.en.json +0 -1
  224. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +0 -1
  225. package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
  226. package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
  227. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  228. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  229. package/templates/.agents/skills/review-code/reference/output-templates.en.md +4 -4
  230. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +4 -4
  231. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  232. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
  233. package/templates/.agents/skills/review-plan/config/verify.en.json +0 -1
  234. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +0 -1
  235. package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
  236. package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
  237. package/templates/.agents/skills/review-pr/SKILL.en.md +2 -2
  238. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -2
  239. package/templates/.agents/skills/review-pr/config/verify.en.json +0 -1
  240. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +0 -1
  241. package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +1 -1
  242. package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +1 -1
  243. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +0 -1
  244. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +0 -1
  245. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  246. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  247. package/templates/.agents/skills/test/SKILL.en.md +0 -3
  248. package/templates/.agents/skills/test/SKILL.zh-CN.md +0 -3
  249. package/templates/.agents/skills/test-integration/SKILL.en.md +0 -3
  250. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +0 -3
  251. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +13 -6
  252. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +0 -3
  253. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +0 -3
  254. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  255. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  256. package/templates/.agents/skills/watch-pr/config/verify.json +0 -1
  257. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
  258. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
  259. package/templates/.agents/templates/handoff.en.md +1 -1
  260. package/templates/.agents/templates/handoff.zh-CN.md +1 -1
  261. package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
  262. package/templates/.agents/workflows/feature-development.en.yaml +1 -1
  263. package/templates/.agents/workflows/refactoring.en.yaml +1 -1
  264. package/dist/lib/task/task-execution-lock.js +0 -250
  265. package/lib/task/task-execution-lock.ts +0 -320
@@ -0,0 +1,132 @@
1
+ import { runSummaryContentPipeline } from './summary-content-pipeline.ts';
2
+
3
+ export type DeliverySummaryContent = Readonly<{
4
+ taskId: string; title: string; summary: string; artifacts: readonly string[];
5
+ verification: Readonly<{ state: string; detail: string }>;
6
+ }>;
7
+ export type CancelSummaryContent = Readonly<{
8
+ taskId: string; title: string; outcome: 'cancelled'; reason: string; cancelledAt: string;
9
+ lastCompletedArtifact: string | null;
10
+ }>;
11
+
12
+ export type DeliverySummarySources = Readonly<{
13
+ taskId: string;
14
+ title: string;
15
+ artifacts: readonly string[];
16
+ verificationFallback?: Readonly<{ state: string; detail: string }>;
17
+ }>;
18
+
19
+ function text(value: unknown, name: string): string {
20
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-empty string`);
21
+ return value.trim();
22
+ }
23
+
24
+ function isMissingText(value: unknown): boolean {
25
+ return typeof value !== 'string' || !value.trim();
26
+ }
27
+
28
+ function fillText(input: Record<string, unknown>, key: string, fallback: string): void {
29
+ if (isMissingText(input[key])) input[key] = fallback;
30
+ }
31
+
32
+ function fillDeliveryVerification(
33
+ input: Record<string, unknown>, fallback: Readonly<{ state: string; detail: string }>
34
+ ): void {
35
+ if (!input.verification || typeof input.verification !== 'object' || Array.isArray(input.verification)) {
36
+ input.verification = fallback;
37
+ return;
38
+ }
39
+ const verification = { ...(input.verification as Record<string, unknown>) };
40
+ fillText(verification, 'state', fallback.state);
41
+ fillText(verification, 'detail', fallback.detail);
42
+ input.verification = verification;
43
+ }
44
+
45
+ function isStringArray(value: unknown): value is string[] {
46
+ return Array.isArray(value) && value.every((item) => typeof item === 'string' && item.trim());
47
+ }
48
+
49
+ export function parseDeliverySummaryContent(value: unknown): DeliverySummaryContent {
50
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('delivery summary must be an object');
51
+ const input = value as Record<string, unknown>;
52
+ if (!Array.isArray(input.artifacts) || input.artifacts.some((item) => typeof item !== 'string')) throw new Error('artifacts must be a string array');
53
+ if (!input.verification || typeof input.verification !== 'object' || Array.isArray(input.verification)) throw new Error('verification must be an object');
54
+ const verification = input.verification as Record<string, unknown>;
55
+ return {
56
+ taskId: text(input.taskId, 'taskId'), title: text(input.title, 'title'), summary: text(input.summary, 'summary'),
57
+ artifacts: input.artifacts.map((item) => text(item, 'artifacts[]')),
58
+ verification: { state: text(verification.state, 'verification.state'), detail: text(verification.detail, 'verification.detail') }
59
+ };
60
+ }
61
+
62
+ function correctDeliverySummaryContent(value: unknown, sources: DeliverySummarySources): unknown {
63
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('delivery summary must be an object');
64
+ const input = { ...(value as Record<string, unknown>) };
65
+ fillText(input, 'taskId', sources.taskId);
66
+ if (input.taskId !== sources.taskId) throw new Error('taskId does not match the active task');
67
+ fillText(input, 'title', sources.title);
68
+ if (!isStringArray(input.artifacts)) input.artifacts = [...sources.artifacts];
69
+ if (sources.verificationFallback) fillDeliveryVerification(input, sources.verificationFallback);
70
+ return input;
71
+ }
72
+
73
+ export function prepareAndRenderDeliverySummaryContent(value: unknown, sources: DeliverySummarySources) {
74
+ return runSummaryContentPipeline(value, {
75
+ validate: parseDeliverySummaryContent,
76
+ correct: (input) => correctDeliverySummaryContent(input, sources),
77
+ render: renderDeliverySummaryContent
78
+ });
79
+ }
80
+
81
+ export function renderDeliverySummaryContent(value: DeliverySummaryContent): string {
82
+ const content = parseDeliverySummaryContent(value);
83
+ return [
84
+ `# ${content.title}`, '', content.summary, '', '## 交付产物', '',
85
+ ...(content.artifacts.length ? content.artifacts.map((item) => `- ${item}`) : ['- 无']), '',
86
+ '## 验证', '', `状态:${content.verification.state}`, '', content.verification.detail, ''
87
+ ].join('\n');
88
+ }
89
+
90
+ export function parseCancelSummaryContent(value: unknown): CancelSummaryContent {
91
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('cancel summary must be an object');
92
+ const input = value as Record<string, unknown>;
93
+ if (input.outcome !== 'cancelled') throw new Error('outcome must be cancelled');
94
+ if (input.lastCompletedArtifact !== null && typeof input.lastCompletedArtifact !== 'string') throw new Error('lastCompletedArtifact must be a string or null');
95
+ return {
96
+ taskId: text(input.taskId, 'taskId'), title: text(input.title, 'title'), outcome: 'cancelled',
97
+ reason: text(input.reason, 'reason'), cancelledAt: text(input.cancelledAt, 'cancelledAt'),
98
+ lastCompletedArtifact: input.lastCompletedArtifact === null ? null : text(input.lastCompletedArtifact, 'lastCompletedArtifact')
99
+ };
100
+ }
101
+
102
+ function correctCancelSummaryContent(value: unknown, sources: Readonly<{
103
+ taskId: string; title: string; reason: string; cancelledAt: string; lastCompletedArtifact: string | null;
104
+ }>): unknown {
105
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('cancel summary must be an object');
106
+ const input = { ...(value as Record<string, unknown>) };
107
+ fillText(input, 'taskId', sources.taskId);
108
+ if (input.taskId !== sources.taskId) throw new Error('taskId does not match the task');
109
+ fillText(input, 'title', sources.title);
110
+ fillText(input, 'reason', sources.reason);
111
+ fillText(input, 'cancelledAt', sources.cancelledAt);
112
+ if (input.lastCompletedArtifact === undefined
113
+ || (input.lastCompletedArtifact !== null && (typeof input.lastCompletedArtifact !== 'string' || !input.lastCompletedArtifact.trim()))) {
114
+ input.lastCompletedArtifact = sources.lastCompletedArtifact;
115
+ }
116
+ return input;
117
+ }
118
+
119
+ export function prepareAndRenderCancelSummaryContent(value: unknown, sources: Readonly<{
120
+ taskId: string; title: string; reason: string; cancelledAt: string; lastCompletedArtifact: string | null;
121
+ }>) {
122
+ return runSummaryContentPipeline(value, {
123
+ validate: parseCancelSummaryContent,
124
+ correct: (input) => correctCancelSummaryContent(input, sources),
125
+ render: renderCancelSummaryContent
126
+ });
127
+ }
128
+
129
+ export function renderCancelSummaryContent(value: CancelSummaryContent): string {
130
+ const content = parseCancelSummaryContent(value);
131
+ return [`# ${content.title}`, '', `结果:${content.outcome}`, '', `取消时间:${content.cancelledAt}`, '', `原因:${content.reason}`, '', `最近完成产物:${content.lastCompletedArtifact ?? '无'}`, ''].join('\n');
132
+ }
@@ -7,7 +7,7 @@ import { isResourceIdentity } from './resource-identity.ts';
7
7
  import type { ResourceIdentity } from './resource-identity.ts';
8
8
 
9
9
  const REPORT_FILE_NAME = 'pr-change-report.json';
10
- const REPORT_VERSION = 2 as const;
10
+ const REPORT_VERSION = 3 as const;
11
11
  const CANONICAL_REPORT_PLACEHOLDER = '<!-- canonical-pr-change-report -->';
12
12
  const CANONICAL_REPORT_HEADING = '### PR 代码增减';
13
13
  const PRECHECK_IDS = [
@@ -71,11 +71,21 @@ type Evidence = {
71
71
  detail: string;
72
72
  };
73
73
 
74
+ type FunctionalGroup = {
75
+ id: string;
76
+ target: string;
77
+ behaviorChange: string;
78
+ necessity: string;
79
+ files: string[];
80
+ tests: { coverage: string; cost: string };
81
+ };
82
+
74
83
  type Precheck = {
75
84
  id: PrecheckId;
76
85
  verdict: CheckVerdict;
77
86
  evidence: Evidence[];
78
87
  rationale: string;
88
+ groups?: FunctionalGroup[];
79
89
  };
80
90
 
81
91
  type PrecheckCandidate = {
@@ -213,25 +223,136 @@ function parseEvidence(value: unknown, checkId: string, index: number): Validati
213
223
  return { ok: true, value: { path: value.path, startLine, endLine, detail: value.detail } };
214
224
  }
215
225
 
216
- function parseChecks(value: unknown, prefix = 'precheck.checks'): ValidationResult<Precheck[]> {
226
+ function parseFunctionalGroupFiles(value: unknown, prefix: string): ValidationResult<string[]> {
227
+ if (!Array.isArray(value) || value.length === 0) return invalid(`${prefix} must be a non-empty array`);
228
+ const files: string[] = [];
229
+ for (let index = 0; index < value.length; index += 1) {
230
+ const file = value[index];
231
+ if (!isSafeRenderedText(file)) return invalid(`${prefix}[${index}] must be a non-empty string`);
232
+ files.push(file.trim());
233
+ }
234
+ return { ok: true, value: files };
235
+ }
236
+
237
+ function parseFunctionalGroupTests(value: unknown, prefix: string): ValidationResult<FunctionalGroup['tests']> {
238
+ if (!isRecord(value)) return invalid(`${prefix} must be an object`);
239
+ if (!isSafeRenderedText(value.coverage)) return invalid(`${prefix}.coverage is required`);
240
+ if (!isSafeRenderedText(value.cost)) return invalid(`${prefix}.cost is required`);
241
+ return { ok: true, value: { coverage: value.coverage.trim(), cost: value.cost.trim() } };
242
+ }
243
+
244
+ function parseFunctionalGroup(value: unknown, prefix: string, ids: Set<string>): ValidationResult<FunctionalGroup> {
245
+ if (!isRecord(value)) return invalid(`${prefix} must be an object`);
246
+ if (Object.hasOwn(value, 'additions') || Object.hasOwn(value, 'deletions') || Object.hasOwn(value, 'net') || Object.hasOwn(value, 'counts')) {
247
+ return invalid(`${prefix} line counts are derived from diff files and must not be supplied`);
248
+ }
249
+ if (!isSafeRenderedText(value.id) || ids.has(value.id.trim())) return invalid(`${prefix}.id must be non-empty and unique`);
250
+ if (!isSafeRenderedText(value.target)) return invalid(`${prefix}.target is required`);
251
+ if (!isSafeRenderedText(value.behaviorChange)) return invalid(`${prefix}.behaviorChange is required`);
252
+ if (!isSafeRenderedText(value.necessity)) return invalid(`${prefix}.necessity is required`);
253
+ const files = parseFunctionalGroupFiles(value.files, `${prefix}.files`);
254
+ if (!files.ok) return files;
255
+ const tests = parseFunctionalGroupTests(value.tests, `${prefix}.tests`);
256
+ if (!tests.ok) return tests;
257
+ const id = value.id.trim();
258
+ ids.add(id);
259
+ return {
260
+ ok: true,
261
+ value: {
262
+ id, target: value.target.trim(), behaviorChange: value.behaviorChange.trim(),
263
+ necessity: value.necessity.trim(), files: files.value, tests: tests.value
264
+ }
265
+ };
266
+ }
267
+
268
+ function parseFunctionalGroups(value: unknown, prefix: string): ValidationResult<FunctionalGroup[]> {
269
+ if (!Array.isArray(value) || value.length === 0) return invalid(`${prefix} must be a non-empty array`);
270
+ const groups: FunctionalGroup[] = [];
271
+ const ids = new Set<string>();
272
+ for (let index = 0; index < value.length; index += 1) {
273
+ const group = parseFunctionalGroup(value[index], `${prefix}[${index}]`, ids);
274
+ if (!group.ok) return group;
275
+ groups.push(group.value);
276
+ }
277
+ return { ok: true, value: groups };
278
+ }
279
+
280
+ function indexDiffFiles(files: ChangeFile[]): ValidationResult<Map<string, ChangeFile>> {
281
+ const fileByPath = new Map<string, ChangeFile>();
282
+ for (const file of files) {
283
+ const key = file.oldPath ?? file.newPath;
284
+ if (!key || fileByPath.has(key)) return invalid(`diff file path '${key ?? ''}' is missing or duplicated`);
285
+ fileByPath.set(key, file);
286
+ }
287
+ return { ok: true, value: fileByPath };
288
+ }
289
+
290
+ function collectGroupMembership(
291
+ groups: FunctionalGroup[], fileByPath: Map<string, ChangeFile>
292
+ ): ValidationResult<Set<string>> {
293
+ const assigned = new Set<string>();
294
+ for (const group of groups) {
295
+ for (const member of group.files) {
296
+ const file = fileByPath.get(member);
297
+ if (!file) return invalid(`change-composition group '${group.id}' references unknown diff file '${member}'`);
298
+ if (assigned.has(member)) return invalid(`diff file '${member}' belongs to more than one functional group`);
299
+ assigned.add(member);
300
+ }
301
+ }
302
+ return { ok: true, value: assigned };
303
+ }
304
+
305
+ function validateGroupMembership(checks: Precheck[], files: ChangeFile[]): ValidationResult<Precheck[]> {
306
+ const composition = checks.find((check) => check.id === 'change-composition');
307
+ if (!composition?.groups) return invalid('precheck.checks.change-composition.groups is required');
308
+ const indexed = indexDiffFiles(files);
309
+ if (!indexed.ok) return indexed;
310
+ const assigned = collectGroupMembership(composition.groups, indexed.value);
311
+ if (!assigned.ok) return assigned;
312
+ const missing = [...indexed.value.keys()].find((key) => !assigned.value.has(key));
313
+ if (missing) return invalid(`diff file '${missing}' has no functional group`);
314
+ return { ok: true, value: checks };
315
+ }
316
+
317
+ function parseEvidenceList(value: unknown, checkId: PrecheckId, prefix: string): ValidationResult<Evidence[]> {
318
+ if (!Array.isArray(value) || value.length === 0) return invalid(`${prefix}.${checkId}.evidence must not be empty`);
319
+ const evidence: Evidence[] = [];
320
+ for (let index = 0; index < value.length; index += 1) {
321
+ const parsed = parseEvidence(value[index], checkId, index);
322
+ if (!parsed.ok) return parsed;
323
+ evidence.push(parsed.value);
324
+ }
325
+ return { ok: true, value: evidence };
326
+ }
327
+
328
+ function parseCheck(value: unknown, index: number, prefix: string): ValidationResult<Precheck> {
329
+ const expectedId = PRECHECK_IDS[index]!;
330
+ const entryPrefix = `${prefix}[${index}]`;
331
+ const at = `${prefix}.${expectedId}`;
332
+ if (!isRecord(value)) return invalid(`${entryPrefix} must be an object`);
333
+ if (value.id !== expectedId) return invalid(`${entryPrefix}.id must be '${expectedId}'`);
334
+ if (value.verdict !== 'pass' && value.verdict !== 'needs-review') return invalid(`${at}.verdict is invalid`);
335
+ const evidence = parseEvidenceList(value.evidence, expectedId, prefix);
336
+ if (!evidence.ok) return evidence;
337
+ if (!isSafeRenderedText(value.rationale)) return invalid(`${at}.rationale is required`);
338
+ if (expectedId !== 'change-composition') {
339
+ if (Object.hasOwn(value, 'groups')) return invalid(`${at}.groups is not allowed`);
340
+ return { ok: true, value: { id: expectedId, verdict: value.verdict, evidence: evidence.value, rationale: value.rationale } };
341
+ }
342
+ const groups = parseFunctionalGroups(value.groups, `${at}.groups`);
343
+ if (!groups.ok) return groups;
344
+ return { ok: true, value: { id: expectedId, verdict: value.verdict, evidence: evidence.value, rationale: value.rationale, groups: groups.value } };
345
+ }
346
+
347
+ function parseChecks(value: unknown, prefix = 'precheck.checks', diffFiles?: ChangeFile[]): ValidationResult<Precheck[]> {
217
348
  if (!Array.isArray(value) || value.length !== PRECHECK_IDS.length) return invalid(`${prefix} must contain exactly six checks`);
218
349
  const checks: Precheck[] = [];
219
350
  for (let index = 0; index < value.length; index += 1) {
220
- const raw = value[index];
221
- if (!isRecord(raw)) return invalid(`${prefix}[${index}] must be an object`);
222
- const expectedId = PRECHECK_IDS[index]!;
223
- if (raw.id !== expectedId) return invalid(`${prefix}[${index}].id must be '${expectedId}'`);
224
- if (raw.verdict !== 'pass' && raw.verdict !== 'needs-review') return invalid(`${prefix}.${expectedId}.verdict is invalid`);
225
- if (!Array.isArray(raw.evidence) || raw.evidence.length === 0) return invalid(`${prefix}.${expectedId}.evidence must not be empty`);
226
- const evidence: Evidence[] = [];
227
- for (let evidenceIndex = 0; evidenceIndex < raw.evidence.length; evidenceIndex += 1) {
228
- const parsed = parseEvidence(raw.evidence[evidenceIndex], expectedId, evidenceIndex);
229
- if (!parsed.ok) return parsed;
230
- evidence.push(parsed.value);
231
- }
232
- if (!isSafeRenderedText(raw.rationale)) return invalid(`${prefix}.${expectedId}.rationale is required`);
233
- checks.push({ id: expectedId, verdict: raw.verdict, evidence, rationale: raw.rationale });
351
+ const parsed = parseCheck(value[index], index, prefix);
352
+ if (!parsed.ok) return parsed;
353
+ checks.push(parsed.value);
234
354
  }
355
+ if (diffFiles) return validateGroupMembership(checks, diffFiles);
235
356
  return { ok: true, value: checks };
236
357
  }
237
358
 
@@ -306,7 +427,7 @@ function validatePrChangeReport(value: unknown): ValidationResult<PrChangeReport
306
427
  if (!totalsMatchFiles(totals.value, files)) return invalid('change report totals do not match files');
307
428
  if (value.precheck.formalReview !== false || (value.precheck.verdict !== 'clear' && value.precheck.verdict !== 'needs-review') ||
308
429
  (value.precheck.route !== 'watch-pr' && value.precheck.route !== 'review-code')) return invalid('change report precheck envelope is invalid');
309
- const checks = parseChecks(value.precheck.checks);
430
+ const checks = parseChecks(value.precheck.checks, 'precheck.checks', files);
310
431
  if (!checks.ok) return checks;
311
432
  const derivedVerdict: PrecheckVerdict = checks.value.every((check) => check.verdict === 'pass') ? 'clear' : 'needs-review';
312
433
  const derivedRoute: PrecheckRoute = derivedVerdict === 'clear' ? 'watch-pr' : 'review-code';
@@ -556,10 +677,34 @@ function renderEvidence(evidence: Evidence): string {
556
677
  return ` - 证据:<code>${escapeReportText(evidence.path)}</code>${location}:${escapeReportText(evidence.detail)}`;
557
678
  }
558
679
 
559
- function renderPrechecks(checks: Precheck[]): string[] {
680
+ function renderFunctionalGroups(groups: FunctionalGroup[], files: ChangeFile[]): string[] {
681
+ const filesByPath = new Map(files.map((file) => [file.oldPath ?? file.newPath!, file]));
682
+ return groups.flatMap((group) => {
683
+ const members = group.files.map((member) => filesByPath.get(member)!);
684
+ const additions = members.reduce((total, file) => total + (file.additions ?? 0), 0);
685
+ const deletions = members.reduce((total, file) => total + (file.deletions ?? 0), 0);
686
+ const unknownFiles = members.filter((file) => file.additions === null).length;
687
+ return [
688
+ ` - **功能组 ${escapeReportText(group.id)}**:${escapeReportText(group.target)}`,
689
+ ` - 行为变化:${escapeReportText(group.behaviorChange)}`,
690
+ ` - 必要性:${escapeReportText(group.necessity)}`,
691
+ ` - 文件:${group.files.map((member) => {
692
+ const file = filesByPath.get(member)!;
693
+ return file.oldPath && file.newPath && file.oldPath !== file.newPath
694
+ ? `<code>${escapeReportText(file.oldPath)}</code> → <code>${escapeReportText(file.newPath)}</code>`
695
+ : `<code>${escapeReportText(member)}</code>`;
696
+ }).join('、')}`,
697
+ ` - 可计数行数(由正式 diff 逐文件推导):新增 ${additions}、删除 ${deletions}、净增 ${additions - deletions}${unknownFiles ? `;另有 ${unknownFiles} 个文件行数不可计` : ''}。`,
698
+ ` - 测试覆盖:${escapeReportText(group.tests.coverage)};成本:${escapeReportText(group.tests.cost)}`
699
+ ];
700
+ });
701
+ }
702
+
703
+ function renderPrechecks(checks: Precheck[], files: ChangeFile[]): string[] {
560
704
  return checks.flatMap((check) => [
561
705
  `- **${PRECHECK_LABELS[check.id]}(${check.id})**:${check.verdict === 'pass' ? '通过' : '需复核'}。${escapeReportText(check.rationale)}`,
562
- ...check.evidence.map(renderEvidence)
706
+ ...check.evidence.map(renderEvidence),
707
+ ...(check.groups ? renderFunctionalGroups(check.groups, files) : [])
563
708
  ]);
564
709
  }
565
710
 
@@ -593,7 +738,7 @@ function renderCanonicalChangeReport(report: PrChangeReport): string {
593
738
  '',
594
739
  '#### 适宜性预检',
595
740
  `- 结论:${precheckSummary};${route};正式审查:否。`,
596
- ...renderPrechecks(report.precheck.checks)
741
+ ...renderPrechecks(report.precheck.checks, report.diff.files)
597
742
  ].join('\n');
598
743
  }
599
744
 
@@ -0,0 +1,60 @@
1
+ import { CANONICAL_REPORT_PLACEHOLDER } from './pr-change-report.ts';
2
+ import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER } from '../task/manual-validation-receipt.ts';
3
+
4
+ export type PrSummaryContent = Readonly<{
5
+ overview: string;
6
+ tests: Readonly<{ state: string; detail: string }>;
7
+ review: Readonly<{ state: string; detail: string }>;
8
+ manualValidation: Readonly<{ state: string; detail: string }>;
9
+ humanDecisions: Readonly<{ open: number; resolved: number; detail: string }>;
10
+ risks: string;
11
+ }>;
12
+
13
+ function nonEmpty(value: unknown, field: string): string {
14
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${field} must be a non-empty string`);
15
+ return value.trim();
16
+ }
17
+
18
+ export function parsePrSummaryContent(value: unknown): PrSummaryContent {
19
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('PR summary must be an object');
20
+ const input = value as Record<string, unknown>;
21
+ const pair = (key: 'tests' | 'review' | 'manualValidation') => {
22
+ const item = input[key];
23
+ if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error(`${key} must be an object`);
24
+ const fields = item as Record<string, unknown>;
25
+ return { state: nonEmpty(fields.state, `${key}.state`), detail: nonEmpty(fields.detail, `${key}.detail`) };
26
+ };
27
+ const decisions = input.humanDecisions;
28
+ if (!decisions || typeof decisions !== 'object' || Array.isArray(decisions)) throw new Error('humanDecisions must be an object');
29
+ const counts = decisions as Record<string, unknown>;
30
+ for (const key of ['open', 'resolved'] as const) {
31
+ if (!Number.isSafeInteger(counts[key]) || Number(counts[key]) < 0) throw new Error(`humanDecisions.${key} must be a non-negative integer`);
32
+ }
33
+ return {
34
+ overview: nonEmpty(input.overview, 'overview'),
35
+ tests: pair('tests'), review: pair('review'), manualValidation: pair('manualValidation'),
36
+ humanDecisions: { open: counts.open as number, resolved: counts.resolved as number, detail: nonEmpty(counts.detail, 'humanDecisions.detail') },
37
+ risks: nonEmpty(input.risks, 'risks')
38
+ };
39
+ }
40
+
41
+ export function renderPrSummaryContent(value: PrSummaryContent): string {
42
+ const content = parsePrSummaryContent(value);
43
+ const manualValidationHeading = content.manualValidation.state === 'required' ? '⚠️ 需人工校验'
44
+ : content.manualValidation.state === 'pending' ? '⏳ 人工验证待收尾'
45
+ : content.manualValidation.state === 'passed' ? '✅ 人工验证已通过'
46
+ : ['none', 'not-required'].includes(content.manualValidation.state) ? '✅ 无需人工校验'
47
+ : content.manualValidation.state === 'unavailable' ? '⚠️ 人工校验状态不可用'
48
+ : content.manualValidation.state === 'not-observed' ? 'ℹ️ 尚未读取人工校验状态'
49
+ : `人工校验状态:${content.manualValidation.state}`;
50
+ return [
51
+ '## 审查摘要', '', content.overview, '',
52
+ '### 关键技术决策', '', '(见审查历程)', '',
53
+ '### 审查历程', '', `状态:${content.review.state}`, '', content.review.detail, '',
54
+ '### 测试结果', '', `状态:${content.tests.state}`, '', content.tests.detail, '',
55
+ `### ${manualValidationHeading}`, '', `状态:${content.manualValidation.state}`, '', content.manualValidation.detail,
56
+ ...(content.manualValidation.state === 'passed' ? ['', MANUAL_VALIDATION_RECEIPT_PLACEHOLDER] : []), '',
57
+ '### 人工决策', '', `未解决:${content.humanDecisions.open};已解决:${content.humanDecisions.resolved}`, '', content.humanDecisions.detail, '',
58
+ '### 风险', '', content.risks, '', CANONICAL_REPORT_PLACEHOLDER, ''
59
+ ].join('\n');
60
+ }