@fitlab-ai/agent-infra 0.9.12 → 0.9.13

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 (239) hide show
  1. package/README.md +11 -11
  2. package/README.zh-CN.md +11 -11
  3. package/dist/bin/internal-cli.js +24 -19
  4. package/dist/lib/agent-clients/next-steps.js +6 -1
  5. package/dist/lib/decide.js +3 -3
  6. package/dist/lib/internal/platform-checks.js +3 -3
  7. package/dist/lib/internal/platform-comment.js +9 -9
  8. package/dist/lib/internal/platform-context.js +2 -2
  9. package/dist/lib/internal/platform-issue.js +8 -9
  10. package/dist/lib/internal/platform-pr-review.js +10 -10
  11. package/dist/lib/internal/platform-pr.js +63 -24
  12. package/dist/lib/internal/platform-release-notes.js +3 -3
  13. package/dist/lib/internal/pr-review-grade.js +11 -9
  14. package/dist/lib/internal/release-workflow.js +4 -4
  15. package/dist/lib/internal/sandbox-control.js +30 -4
  16. package/dist/lib/internal/task-activity.js +3 -3
  17. package/dist/lib/internal/task-artifact.js +76 -24
  18. package/dist/lib/internal/task-create.js +2 -2
  19. package/dist/lib/internal/task-event.js +6 -3
  20. package/dist/lib/internal/task-finalization.js +2 -2
  21. package/dist/lib/internal/task-invalidation.js +60 -0
  22. package/dist/lib/internal/task-ledger.js +9 -6
  23. package/dist/lib/internal/task-lifecycle.js +2 -2
  24. package/dist/lib/internal/task-override.js +4 -4
  25. package/dist/lib/internal/task-review.js +3 -3
  26. package/dist/lib/internal/task-verify.js +6 -6
  27. package/dist/lib/internal/task-warning.js +3 -3
  28. package/dist/lib/platform/adapters.js +1 -3
  29. package/dist/lib/platform/change-request-git-evidence.js +27 -7
  30. package/dist/lib/platform/completion-backfill.js +6 -5
  31. package/dist/lib/platform/context.js +70 -130
  32. package/dist/lib/platform/github-client.js +1 -1
  33. package/dist/lib/platform/github-provider.js +903 -0
  34. package/dist/lib/platform/in-label-sync.js +143 -0
  35. package/dist/lib/platform/issue-comments.js +94 -41
  36. package/dist/lib/platform/issues.js +335 -189
  37. package/dist/lib/platform/metadata-labels.js +6 -2
  38. package/dist/lib/platform/none-provider.js +28 -0
  39. package/dist/lib/platform/pr-change-report.js +551 -0
  40. package/dist/lib/platform/pr-checks.js +119 -104
  41. package/dist/lib/platform/pr-review.js +88 -72
  42. package/dist/lib/platform/pr-summary.js +520 -50
  43. package/dist/lib/platform/provider-bridge.js +45 -0
  44. package/dist/lib/platform/provider-contract.d.ts +487 -0
  45. package/dist/lib/platform/provider-contract.js +95 -0
  46. package/dist/lib/platform/provider-loader.js +251 -0
  47. package/dist/lib/platform/provider-validation.js +504 -0
  48. package/dist/lib/platform/pull-request-metadata.js +8 -6
  49. package/dist/lib/platform/pull-requests.js +783 -537
  50. package/dist/lib/platform/release-notes.js +78 -55
  51. package/dist/lib/platform/releases.js +79 -50
  52. package/dist/lib/platform/resource-identity.d.ts +26 -0
  53. package/dist/lib/platform/resource-identity.js +92 -0
  54. package/dist/lib/platform/task-identities.js +31 -0
  55. package/dist/lib/platform/verification-required.js +11 -12
  56. package/dist/lib/platform/verification-sync.js +112 -238
  57. package/dist/lib/pr-review/evidence-grading.js +19 -6
  58. package/dist/lib/run/index.js +63 -23
  59. package/dist/lib/sandbox/control/client.js +96 -15
  60. package/dist/lib/sandbox/control/executor.js +3 -3
  61. package/dist/lib/sandbox/control/protocol.js +77 -0
  62. package/dist/lib/sandbox/control/server.js +398 -25
  63. package/dist/lib/sandbox/control/state.js +204 -1
  64. package/dist/lib/server/protocol.js +38 -1
  65. package/dist/lib/task/activity-log.js +8 -1
  66. package/dist/lib/task/artifact-lifecycle.js +33 -5
  67. package/dist/lib/task/capabilities.js +239 -0
  68. package/dist/lib/task/commands/status.js +32 -0
  69. package/dist/lib/task/commit-operation.js +12 -1
  70. package/dist/lib/task/control-authority.js +8 -5
  71. package/dist/lib/task/create-service.js +8 -8
  72. package/dist/lib/task/current-contract.js +5 -1
  73. package/dist/lib/task/decision-details.js +1 -1
  74. package/dist/lib/task/events.js +297 -17
  75. package/dist/lib/task/finalization.js +37 -13
  76. package/dist/lib/task/human-override.js +58 -36
  77. package/dist/lib/task/invalidation-command.js +86 -0
  78. package/dist/lib/task/invalidation.js +260 -0
  79. package/dist/lib/task/ledger-intents.js +37 -0
  80. package/dist/lib/task/local-artifact-finalization.js +374 -0
  81. package/dist/lib/task/orchestration.js +49 -93
  82. package/dist/lib/task/pr-delivery-fact.js +102 -101
  83. package/dist/lib/task/review-fingerprint.js +24 -2
  84. package/dist/lib/task/rework-intent.js +138 -0
  85. package/dist/lib/task/skill-scope.js +23 -0
  86. package/dist/lib/task/task-execution-lock.js +27 -17
  87. package/dist/lib/task/verification-engine.js +43 -16
  88. package/dist/lib/task/verification.js +3 -3
  89. package/dist/lib/task/workflow-warning-intents.js +1 -1
  90. package/dist/lib/task/write.js +11 -0
  91. package/dist/lib/version.js +11 -1
  92. package/dist/lifecycle-build-manifest.json +3 -3
  93. package/lib/agent-clients/next-steps.ts +6 -1
  94. package/lib/decide.ts +3 -3
  95. package/lib/init.ts +1 -1
  96. package/lib/internal/platform-checks.ts +3 -3
  97. package/lib/internal/platform-comment.ts +8 -8
  98. package/lib/internal/platform-context.ts +2 -2
  99. package/lib/internal/platform-issue.ts +7 -8
  100. package/lib/internal/platform-pr-review.ts +9 -9
  101. package/lib/internal/platform-pr.ts +47 -22
  102. package/lib/internal/platform-release-notes.ts +3 -3
  103. package/lib/internal/pr-review-grade.ts +10 -8
  104. package/lib/internal/release-workflow.ts +4 -4
  105. package/lib/internal/sandbox-control.ts +27 -3
  106. package/lib/internal/task-activity.ts +3 -3
  107. package/lib/internal/task-artifact.ts +65 -22
  108. package/lib/internal/task-create.ts +2 -2
  109. package/lib/internal/task-event.ts +6 -3
  110. package/lib/internal/task-finalization.ts +3 -3
  111. package/lib/internal/task-invalidation.ts +39 -0
  112. package/lib/internal/task-ledger.ts +11 -8
  113. package/lib/internal/task-lifecycle.ts +2 -2
  114. package/lib/internal/task-override.ts +6 -6
  115. package/lib/internal/task-review.ts +3 -3
  116. package/lib/internal/task-verify.ts +6 -6
  117. package/lib/internal/task-warning.ts +3 -3
  118. package/lib/platform/adapters.ts +3 -2
  119. package/lib/platform/change-request-git-evidence.ts +36 -9
  120. package/lib/platform/completion-backfill.ts +9 -8
  121. package/lib/platform/context.ts +97 -134
  122. package/lib/platform/github-client.ts +2 -2
  123. package/lib/platform/github-provider.ts +910 -0
  124. package/lib/platform/in-label-sync.ts +208 -0
  125. package/lib/platform/issue-comments.ts +97 -47
  126. package/lib/platform/issues.ts +344 -174
  127. package/lib/platform/metadata-labels.ts +5 -2
  128. package/lib/platform/none-provider.ts +35 -0
  129. package/lib/platform/pr-change-report.ts +645 -0
  130. package/lib/platform/pr-checks.ts +126 -106
  131. package/lib/platform/pr-review.ts +88 -85
  132. package/lib/platform/pr-summary.ts +560 -53
  133. package/lib/platform/provider-bridge.ts +82 -0
  134. package/lib/platform/provider-contract.ts +536 -0
  135. package/lib/platform/provider-loader.ts +307 -0
  136. package/lib/platform/provider-validation.ts +529 -0
  137. package/lib/platform/pull-request-metadata.ts +10 -7
  138. package/lib/platform/pull-requests.ts +826 -543
  139. package/lib/platform/release-notes.ts +76 -59
  140. package/lib/platform/releases.ts +77 -43
  141. package/lib/platform/resource-identity.ts +121 -0
  142. package/lib/platform/task-identities.ts +40 -0
  143. package/lib/platform/types.ts +4 -2
  144. package/lib/platform/verification-required.ts +11 -13
  145. package/lib/platform/verification-sync.ts +112 -292
  146. package/lib/pr-review/evidence-grading.ts +25 -8
  147. package/lib/run/index.ts +57 -22
  148. package/lib/sandbox/control/client.ts +105 -15
  149. package/lib/sandbox/control/executor.ts +4 -4
  150. package/lib/sandbox/control/protocol.ts +113 -0
  151. package/lib/sandbox/control/server.ts +431 -23
  152. package/lib/sandbox/control/state.ts +251 -1
  153. package/lib/server/protocol.ts +36 -1
  154. package/lib/task/activity-log.ts +8 -0
  155. package/lib/task/artifact-lifecycle.ts +32 -6
  156. package/lib/task/capabilities.ts +262 -0
  157. package/lib/task/commands/status.ts +38 -0
  158. package/lib/task/commit-operation.ts +12 -1
  159. package/lib/task/control-authority.ts +11 -8
  160. package/lib/task/create-service.ts +8 -8
  161. package/lib/task/current-contract.ts +6 -1
  162. package/lib/task/decision-details.ts +6 -2
  163. package/lib/task/events.ts +296 -16
  164. package/lib/task/finalization.ts +50 -14
  165. package/lib/task/human-override.ts +70 -46
  166. package/lib/task/invalidation-command.ts +86 -0
  167. package/lib/task/invalidation.ts +332 -0
  168. package/lib/task/ledger-intents.ts +35 -1
  169. package/lib/task/local-artifact-finalization.ts +563 -0
  170. package/lib/task/orchestration.ts +52 -85
  171. package/lib/task/pr-delivery-fact.ts +131 -148
  172. package/lib/task/review-fingerprint.ts +23 -2
  173. package/lib/task/rework-intent.ts +165 -0
  174. package/lib/task/skill-scope.ts +24 -0
  175. package/lib/task/task-execution-lock.ts +23 -14
  176. package/lib/task/verification-engine.ts +52 -17
  177. package/lib/task/verification.ts +4 -4
  178. package/lib/task/workflow-warning-intents.ts +1 -1
  179. package/lib/task/write.ts +19 -0
  180. package/lib/update.ts +1 -1
  181. package/lib/version.ts +15 -1
  182. package/package.json +7 -1
  183. package/templates/.agents/rules/README.en.md +1 -1
  184. package/templates/.agents/rules/README.zh-CN.md +1 -1
  185. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  186. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  187. package/templates/.agents/rules/issue-sync.en.md +2 -0
  188. package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
  189. package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
  190. package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
  191. package/templates/.agents/rules/pr-sync.en.md +10 -4
  192. package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
  193. package/templates/.agents/rules/task-short-id.en.md +1 -4
  194. package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
  195. package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
  196. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
  197. package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
  198. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
  199. package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
  200. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
  201. package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
  202. package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
  203. package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
  204. package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
  205. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
  206. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
  207. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
  208. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
  209. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
  210. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
  211. package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
  212. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
  213. package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
  214. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
  215. package/templates/.agents/skills/create-pr/config/verify.json +1 -0
  216. package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
  217. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
  218. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
  219. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
  220. package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
  221. package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
  222. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
  223. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
  224. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
  225. package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
  226. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
  227. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  228. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  229. package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
  230. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
  231. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
  232. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -2
  233. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  234. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  235. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +6 -0
  236. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
  237. package/templates/.agents/templates/task.en.md +18 -1
  238. package/templates/.agents/templates/task.zh-CN.md +18 -1
  239. package/templates/.github/workflows/pr-label.yml +7 -26
@@ -0,0 +1,563 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import path from 'node:path';
4
+
5
+ import {
6
+ inspectDecisionDetailDuplicates,
7
+ scanVisibleMarkdown
8
+ } from './decision-details.ts';
9
+ import type { VisibleHeading } from './decision-details.ts';
10
+ import {
11
+ parseArtifactName,
12
+ validateCompletedArtifact
13
+ } from './artifact-lifecycle.ts';
14
+ import { resolveTaskRef } from './resolve-ref.ts';
15
+
16
+ type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
17
+
18
+ const LOCAL_ARTIFACT_REQUIRED_SECTIONS: Readonly<Record<LocalArtifactFamily, readonly string[]>> = {
19
+ analysis: [
20
+ '需求来源', '需求理解', '相关文件', '影响评估', '技术风险',
21
+ '工作量和复杂度评估', '状态核对'
22
+ ],
23
+ plan: [
24
+ '问题理解', '约束条件', '方案对比', '技术方法', '实施步骤',
25
+ '文件清单', '验证策略', '状态核对'
26
+ ],
27
+ code: [
28
+ '实现输入', '变更文件', '关键代码说明', '测试结果', '与方案的差异',
29
+ '供审查关注的内容', '状态核对', '证据原文'
30
+ ]
31
+ };
32
+
33
+ const LOCAL_ARTIFACT_REQUIRED_PATTERNS = ['^\\$ '];
34
+
35
+ type LocalArtifactDiagnosticCode =
36
+ | 'LOCAL_ARTIFACT_EMPTY'
37
+ | 'LOCAL_ARTIFACT_MISSING_SECTION'
38
+ | 'LOCAL_ARTIFACT_DUPLICATE_SECTION'
39
+ | 'LOCAL_STATUS_COMMAND_MISSING'
40
+ | 'LOCAL_DECISION_DETAIL_DUPLICATE'
41
+ | 'LOCAL_REQUIRED_PATTERN_MISSING'
42
+ | 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
43
+ | 'LOCAL_REPAIR_PROVENANCE_CONFLICT'
44
+ | 'LOCAL_REPAIR_BASELINE_MISMATCH';
45
+
46
+ type LocalArtifactDiagnostic = {
47
+ code: LocalArtifactDiagnosticCode;
48
+ message: string;
49
+ repairable: boolean;
50
+ line: number | null;
51
+ from?: string;
52
+ to?: string;
53
+ operation?: 'replace-line';
54
+ };
55
+
56
+ type LocalArtifactValidationOptions = {
57
+ family: LocalArtifactFamily;
58
+ requiredSections?: readonly string[];
59
+ requiredPatterns?: readonly string[];
60
+ };
61
+
62
+ type LocalArtifactValidationResult = {
63
+ ok: boolean;
64
+ family: LocalArtifactFamily;
65
+ semanticDigest: string;
66
+ repairable: boolean;
67
+ diagnostics: readonly LocalArtifactDiagnostic[];
68
+ };
69
+
70
+ type LocalArtifactFinalizationRequest = {
71
+ taskRef: string;
72
+ family: LocalArtifactFamily;
73
+ artifact: string;
74
+ repoRoot?: string;
75
+ requiredSections?: readonly string[];
76
+ requiredPatterns?: readonly string[];
77
+ };
78
+
79
+ type LocalArtifactFinalizationResult = {
80
+ status: 'passed' | 'failed';
81
+ changed: false;
82
+ taskId: string | null;
83
+ taskDir: string | null;
84
+ family: LocalArtifactFamily;
85
+ artifact: string;
86
+ artifactSha256: string | null;
87
+ semanticDigest: string | null;
88
+ repairable: boolean;
89
+ diagnostics: readonly LocalArtifactDiagnostic[];
90
+ error: { code: string; message: string } | null;
91
+ };
92
+
93
+ type LocalArtifactFinalizationIntent = Readonly<{
94
+ version: 1;
95
+ taskId: string;
96
+ family: LocalArtifactFamily;
97
+ artifact: string;
98
+ state: 'awaiting-repair' | 'passed' | 'consumed';
99
+ baselineSemanticDigest: string | null;
100
+ artifactSha256: string;
101
+ semanticDigest: string;
102
+ }>;
103
+
104
+ function finalizationIntentRoot(repoRoot: string): string {
105
+ return path.join(repoRoot, '.agents', 'workspace', '.local-artifact-finalization-intents');
106
+ }
107
+
108
+ function finalizationIntentPath(repoRoot: string, taskId: string, family: LocalArtifactFamily, artifact: string): string {
109
+ return path.join(finalizationIntentRoot(repoRoot), `${taskId}-${family}-${artifact}.json`);
110
+ }
111
+
112
+ function isFinalizationIntent(value: unknown): value is LocalArtifactFinalizationIntent {
113
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
114
+ const intent = value as Record<string, unknown>;
115
+ return intent.version === 1
116
+ && typeof intent.taskId === 'string' && intent.taskId.length > 0
117
+ && (intent.family === 'analysis' || intent.family === 'plan' || intent.family === 'code')
118
+ && typeof intent.artifact === 'string' && intent.artifact.length > 0
119
+ && (intent.state === 'awaiting-repair' || intent.state === 'passed' || intent.state === 'consumed')
120
+ && (intent.baselineSemanticDigest === null || (typeof intent.baselineSemanticDigest === 'string' && /^[a-f0-9]{64}$/.test(intent.baselineSemanticDigest)))
121
+ && typeof intent.artifactSha256 === 'string' && /^[a-f0-9]{64}$/.test(intent.artifactSha256)
122
+ && typeof intent.semanticDigest === 'string' && /^[a-f0-9]{64}$/.test(intent.semanticDigest)
123
+ && (intent.state === 'awaiting-repair' ? intent.baselineSemanticDigest === intent.semanticDigest : true);
124
+ }
125
+
126
+ function readLocalArtifactFinalizationIntent(
127
+ repoRoot: string,
128
+ taskId: string,
129
+ family: LocalArtifactFamily,
130
+ artifact: string
131
+ ): LocalArtifactFinalizationIntent | null {
132
+ const target = finalizationIntentPath(repoRoot, taskId, family, artifact);
133
+ if (!fs.existsSync(target)) return null;
134
+ const value = JSON.parse(fs.readFileSync(target, 'utf8')) as unknown;
135
+ if (!isFinalizationIntent(value) || value.taskId !== taskId || value.family !== family || value.artifact !== artifact) {
136
+ throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: finalization provenance schema is invalid');
137
+ }
138
+ return value;
139
+ }
140
+
141
+ function writeLocalArtifactFinalizationIntent(repoRoot: string, value: LocalArtifactFinalizationIntent): void {
142
+ if (!isFinalizationIntent(value)) throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: finalization provenance schema is invalid');
143
+ const directory = finalizationIntentRoot(repoRoot);
144
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
145
+ const target = finalizationIntentPath(repoRoot, value.taskId, value.family, value.artifact);
146
+ const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
147
+ fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
148
+ try { fs.renameSync(temporary, target); }
149
+ catch (error) {
150
+ try { fs.unlinkSync(temporary); } catch { /* preserve primary error */ }
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ function consumeLocalArtifactFinalizationIntent(
156
+ repoRoot: string,
157
+ intent: LocalArtifactFinalizationIntent
158
+ ): LocalArtifactFinalizationIntent {
159
+ if (intent.state === 'consumed') return intent;
160
+ if (intent.state !== 'passed') throw new Error('LOCAL_FINALIZATION_INTENT_INVALID: only passed provenance can be consumed');
161
+ const consumed = { ...intent, state: 'consumed' as const };
162
+ writeLocalArtifactFinalizationIntent(repoRoot, consumed);
163
+ return consumed;
164
+ }
165
+
166
+ function requiredSections(family: LocalArtifactFamily): readonly string[] {
167
+ return LOCAL_ARTIFACT_REQUIRED_SECTIONS[family];
168
+ }
169
+
170
+ function lineNumber(content: string, offset: number): number {
171
+ return content.slice(0, offset).split('\n').length;
172
+ }
173
+
174
+ function sectionBody(content: string, heading: VisibleHeading): string {
175
+ const next = scanVisibleMarkdown(content).headings.find((candidate) => (
176
+ candidate.start > heading.start && candidate.level <= 2
177
+ ));
178
+ return content.slice(heading.end, next?.start ?? content.length);
179
+ }
180
+
181
+ function semanticDigest(content: string, candidate?: VisibleHeading): string {
182
+ let normalized = content;
183
+ if (candidate) {
184
+ const raw = content.slice(candidate.start, candidate.end);
185
+ const marker = raw.match(/([::])\s*$/);
186
+ if (marker?.index !== undefined) {
187
+ const canonical = raw.slice(0, marker.index) + raw.slice(marker.index + 1);
188
+ normalized = `${content.slice(0, candidate.start)}${canonical}${content.slice(candidate.end)}`;
189
+ }
190
+ }
191
+ return createHash('sha256').update(normalized, 'utf8').digest('hex');
192
+ }
193
+
194
+ function sha256Content(content: string): string {
195
+ return createHash('sha256').update(content, 'utf8').digest('hex');
196
+ }
197
+
198
+ function visibleSectionHeadings(content: string, section: string): VisibleHeading[] {
199
+ return scanVisibleMarkdown(content).headings.filter((heading) => (
200
+ heading.level === 2 && heading.text === section
201
+ ));
202
+ }
203
+
204
+ function trailingPunctuationCandidates(content: string, section: string): VisibleHeading[] {
205
+ return scanVisibleMarkdown(content).headings.filter((heading) => (
206
+ heading.level === 2 && (
207
+ heading.text === `${section}:` || heading.text === `${section}:`
208
+ )
209
+ ));
210
+ }
211
+
212
+ function diagnostic(
213
+ code: LocalArtifactDiagnosticCode,
214
+ message: string,
215
+ content: string,
216
+ heading?: VisibleHeading,
217
+ extra: Partial<LocalArtifactDiagnostic> = {}
218
+ ): LocalArtifactDiagnostic {
219
+ return {
220
+ code, message, repairable: false,
221
+ line: heading ? lineNumber(content, heading.start) : null,
222
+ ...extra
223
+ };
224
+ }
225
+
226
+ function isStatusPattern(pattern: string): boolean {
227
+ return pattern === '^\\$ ';
228
+ }
229
+
230
+ function validateLocalArtifact(
231
+ content: string,
232
+ options: LocalArtifactValidationOptions
233
+ ): LocalArtifactValidationResult {
234
+ const sections = options.requiredSections ?? requiredSections(options.family);
235
+ const patterns = options.requiredPatterns ?? LOCAL_ARTIFACT_REQUIRED_PATTERNS;
236
+ const diagnostics: LocalArtifactDiagnostic[] = [];
237
+ const candidates: VisibleHeading[] = [];
238
+ const scanned = scanVisibleMarkdown(content);
239
+
240
+ if (!content.trim()) {
241
+ diagnostics.push(diagnostic('LOCAL_ARTIFACT_EMPTY', 'artifact is empty', content));
242
+ }
243
+
244
+ for (const section of sections) {
245
+ const exact = scanned.headings.filter((heading) => heading.level === 2 && heading.text === section);
246
+ const punctuated = trailingPunctuationCandidates(content, section);
247
+ if (exact.length > 1 || punctuated.length > 0 && exact.length > 0 || punctuated.length > 1) {
248
+ const first = exact[1] ?? punctuated[0] ?? exact[0];
249
+ diagnostics.push(diagnostic(
250
+ 'LOCAL_ARTIFACT_DUPLICATE_SECTION',
251
+ `required section '${section}' is duplicated or has an ambiguous punctuation variant`,
252
+ content, first
253
+ ));
254
+ } else if (exact.length === 0 && punctuated.length === 1) {
255
+ candidates.push(punctuated[0]!);
256
+ } else if (exact.length === 0) {
257
+ diagnostics.push(diagnostic(
258
+ 'LOCAL_ARTIFACT_MISSING_SECTION',
259
+ `required section '${section}' is missing`,
260
+ content
261
+ ));
262
+ }
263
+ }
264
+
265
+ const statusSection = sections.find((section) => section === '状态核对' || section.toLowerCase() === 'state check');
266
+ if (statusSection) {
267
+ const statusHeading = visibleSectionHeadings(content, statusSection)[0]
268
+ ?? candidates.find((heading) => heading.text === `${statusSection}:` || heading.text === `${statusSection}:`);
269
+ if (statusHeading) {
270
+ const body = sectionBody(content, statusHeading);
271
+ for (const pattern of patterns.filter(isStatusPattern)) {
272
+ if (!new RegExp(pattern, 'm').test(body)) {
273
+ diagnostics.push(diagnostic(
274
+ 'LOCAL_STATUS_COMMAND_MISSING',
275
+ `status section '${statusSection}' is missing required command output`,
276
+ content, statusHeading
277
+ ));
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ for (const pattern of patterns.filter((item) => !isStatusPattern(item))) {
284
+ if (!new RegExp(pattern, 'm').test(content)) {
285
+ diagnostics.push(diagnostic(
286
+ 'LOCAL_REQUIRED_PATTERN_MISSING',
287
+ `artifact is missing required pattern '${pattern}'`,
288
+ content
289
+ ));
290
+ }
291
+ }
292
+
293
+ const decisionDetails = inspectDecisionDetailDuplicates(content);
294
+ if (!decisionDetails.ok) {
295
+ diagnostics.push(diagnostic(
296
+ 'LOCAL_DECISION_DETAIL_DUPLICATE',
297
+ decisionDetails.message,
298
+ content,
299
+ decisionDetails.duplicates[0]?.blocks[0]
300
+ ? scanned.headings.find((heading) => heading.start === decisionDetails.duplicates[0]!.blocks[0]!.start)
301
+ : undefined
302
+ ));
303
+ }
304
+
305
+ if (candidates.length === 1 && diagnostics.length === 0) {
306
+ const candidate = candidates[0]!;
307
+ const section = candidate.text.slice(0, -1);
308
+ const repair = diagnostic(
309
+ 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION',
310
+ `visible required H2 '${candidate.text}' may be normalized to '${section}'`,
311
+ content,
312
+ candidate,
313
+ {
314
+ repairable: true,
315
+ from: candidate.text,
316
+ to: section,
317
+ operation: 'replace-line'
318
+ }
319
+ );
320
+ return {
321
+ ok: false,
322
+ family: options.family,
323
+ semanticDigest: semanticDigest(content, candidate),
324
+ repairable: true,
325
+ diagnostics: [repair]
326
+ };
327
+ }
328
+
329
+ if (candidates.length > 0) {
330
+ for (const candidate of candidates) {
331
+ const section = candidate.text.slice(0, -1);
332
+ diagnostics.push(diagnostic(
333
+ 'LOCAL_ARTIFACT_MISSING_SECTION',
334
+ `required section '${section}' is missing and cannot be repaired while other structural defects exist`,
335
+ content, candidate
336
+ ));
337
+ }
338
+ }
339
+
340
+ return {
341
+ ok: diagnostics.length === 0,
342
+ family: options.family,
343
+ semanticDigest: semanticDigest(content),
344
+ repairable: false,
345
+ diagnostics
346
+ };
347
+ }
348
+
349
+ function failedFinalization(
350
+ request: LocalArtifactFinalizationRequest,
351
+ error: { code: string; message: string },
352
+ extra: Partial<LocalArtifactFinalizationResult> = {}
353
+ ): LocalArtifactFinalizationResult {
354
+ return {
355
+ status: 'failed', changed: false,
356
+ taskId: null, taskDir: null,
357
+ family: request.family, artifact: request.artifact,
358
+ artifactSha256: null, semanticDigest: null,
359
+ repairable: false, diagnostics: [], error, ...extra
360
+ };
361
+ }
362
+
363
+ function provenanceFailure(
364
+ request: LocalArtifactFinalizationRequest,
365
+ resolved: { taskId: string; taskDir: string },
366
+ content: string,
367
+ artifactSha256: string,
368
+ semanticDigestValue: string,
369
+ code: LocalArtifactDiagnosticCode,
370
+ message: string
371
+ ): LocalArtifactFinalizationResult {
372
+ return failedFinalization(request, { code, message }, {
373
+ taskId: resolved.taskId,
374
+ taskDir: resolved.taskDir,
375
+ artifactSha256,
376
+ semanticDigest: semanticDigestValue,
377
+ diagnostics: [diagnostic(code, message, content)]
378
+ });
379
+ }
380
+
381
+ function finalizeLocalArtifact(request: LocalArtifactFinalizationRequest): LocalArtifactFinalizationResult {
382
+ const resolved = resolveTaskRef(request.taskRef, { repoRoot: request.repoRoot });
383
+ if (!resolved.ok) return failedFinalization(request, { code: resolved.code, message: resolved.message });
384
+ const parsed = parseArtifactName(request.artifact);
385
+ if (!parsed || parsed.family !== request.family) {
386
+ return failedFinalization(request, {
387
+ code: 'ARTIFACT_IDENTITY_INVALID',
388
+ message: `artifact '${request.artifact}' does not match ${request.family}`
389
+ }, { taskId: resolved.taskId, taskDir: resolved.taskDir });
390
+ }
391
+ const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
392
+ if (!validated.ok) {
393
+ return failedFinalization(request, validated.error, { taskId: resolved.taskId, taskDir: resolved.taskDir });
394
+ }
395
+ let content: string;
396
+ try { content = fs.readFileSync(validated.artifact.path, 'utf8'); }
397
+ catch (error) {
398
+ return failedFinalization(request, { code: 'ARTIFACT_NOT_READABLE', message: String(error) }, {
399
+ taskId: resolved.taskId, taskDir: resolved.taskDir
400
+ });
401
+ }
402
+ const result = validateLocalArtifact(content, {
403
+ family: request.family,
404
+ requiredSections: request.requiredSections,
405
+ requiredPatterns: request.requiredPatterns
406
+ });
407
+ const artifactSha256 = sha256Content(content);
408
+ let intent: LocalArtifactFinalizationIntent | null;
409
+ try {
410
+ intent = readLocalArtifactFinalizationIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
411
+ } catch (error) {
412
+ return failedFinalization(request, {
413
+ code: 'LOCAL_FINALIZATION_INTENT_INVALID',
414
+ message: error instanceof Error ? error.message : String(error)
415
+ }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
416
+ }
417
+ if (result.repairable) {
418
+ if (intent && (intent.state !== 'awaiting-repair' || intent.baselineSemanticDigest !== result.semanticDigest)) {
419
+ return provenanceFailure(
420
+ request, resolved, content, artifactSha256, result.semanticDigest,
421
+ 'LOCAL_REPAIR_PROVENANCE_CONFLICT',
422
+ 'a different local repair baseline is already recorded for this artifact'
423
+ );
424
+ }
425
+ if (!intent) {
426
+ try {
427
+ writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
428
+ version: 1,
429
+ taskId: resolved.taskId,
430
+ family: request.family,
431
+ artifact: request.artifact,
432
+ state: 'awaiting-repair',
433
+ baselineSemanticDigest: result.semanticDigest,
434
+ artifactSha256,
435
+ semanticDigest: result.semanticDigest
436
+ });
437
+ } catch (error) {
438
+ return failedFinalization(request, {
439
+ code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
440
+ message: error instanceof Error ? error.message : String(error)
441
+ }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
442
+ }
443
+ }
444
+ return {
445
+ status: 'failed',
446
+ changed: false,
447
+ taskId: resolved.taskId,
448
+ taskDir: resolved.taskDir,
449
+ family: request.family,
450
+ artifact: request.artifact,
451
+ artifactSha256,
452
+ semanticDigest: result.semanticDigest,
453
+ repairable: true,
454
+ diagnostics: result.diagnostics,
455
+ error: {
456
+ code: 'LOCAL_ARTIFACT_INVALID',
457
+ message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
458
+ }
459
+ };
460
+ }
461
+ if (!result.ok) {
462
+ return {
463
+ status: 'failed',
464
+ changed: false,
465
+ taskId: resolved.taskId,
466
+ taskDir: resolved.taskDir,
467
+ family: request.family,
468
+ artifact: request.artifact,
469
+ artifactSha256,
470
+ semanticDigest: result.semanticDigest,
471
+ repairable: false,
472
+ diagnostics: result.diagnostics,
473
+ error: {
474
+ code: 'LOCAL_ARTIFACT_INVALID',
475
+ message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
476
+ }
477
+ };
478
+ }
479
+ if (intent?.state === 'awaiting-repair' && intent.baselineSemanticDigest !== result.semanticDigest) {
480
+ return provenanceFailure(
481
+ request, resolved, content, artifactSha256, result.semanticDigest,
482
+ 'LOCAL_REPAIR_BASELINE_MISMATCH',
483
+ 'the repaired artifact semantic digest does not match the recorded repair baseline'
484
+ );
485
+ }
486
+ if ((intent?.state === 'passed' || intent?.state === 'consumed')
487
+ && (intent.artifactSha256 !== artifactSha256 || intent.semanticDigest !== result.semanticDigest)) {
488
+ return provenanceFailure(
489
+ request, resolved, content, artifactSha256, result.semanticDigest,
490
+ 'LOCAL_REPAIR_PROVENANCE_CONFLICT',
491
+ 'the artifact changed after its finalization provenance was recorded'
492
+ );
493
+ }
494
+ if (intent?.state === 'consumed') {
495
+ return {
496
+ status: 'passed',
497
+ changed: false,
498
+ taskId: resolved.taskId,
499
+ taskDir: resolved.taskDir,
500
+ family: request.family,
501
+ artifact: request.artifact,
502
+ artifactSha256,
503
+ semanticDigest: result.semanticDigest,
504
+ repairable: false,
505
+ diagnostics: result.diagnostics,
506
+ error: null
507
+ };
508
+ }
509
+ try {
510
+ writeLocalArtifactFinalizationIntent(resolved.repoRoot, {
511
+ version: 1,
512
+ taskId: resolved.taskId,
513
+ family: request.family,
514
+ artifact: request.artifact,
515
+ state: 'passed',
516
+ baselineSemanticDigest: intent?.baselineSemanticDigest ?? null,
517
+ artifactSha256,
518
+ semanticDigest: result.semanticDigest
519
+ });
520
+ } catch (error) {
521
+ return failedFinalization(request, {
522
+ code: 'LOCAL_FINALIZATION_INTENT_WRITE_FAILED',
523
+ message: error instanceof Error ? error.message : String(error)
524
+ }, { taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256, semanticDigest: result.semanticDigest });
525
+ }
526
+ return {
527
+ status: result.ok ? 'passed' : 'failed',
528
+ changed: false,
529
+ taskId: resolved.taskId,
530
+ taskDir: resolved.taskDir,
531
+ family: request.family,
532
+ artifact: request.artifact,
533
+ artifactSha256,
534
+ semanticDigest: result.semanticDigest,
535
+ repairable: result.repairable,
536
+ diagnostics: result.diagnostics,
537
+ error: result.ok ? null : {
538
+ code: 'LOCAL_ARTIFACT_INVALID',
539
+ message: result.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
540
+ }
541
+ };
542
+ }
543
+
544
+ export {
545
+ LOCAL_ARTIFACT_REQUIRED_PATTERNS,
546
+ LOCAL_ARTIFACT_REQUIRED_SECTIONS,
547
+ consumeLocalArtifactFinalizationIntent,
548
+ finalizeLocalArtifact,
549
+ readLocalArtifactFinalizationIntent,
550
+ semanticDigest,
551
+ sha256Content,
552
+ validateLocalArtifact
553
+ };
554
+ export type {
555
+ LocalArtifactDiagnostic,
556
+ LocalArtifactDiagnosticCode,
557
+ LocalArtifactFamily,
558
+ LocalArtifactFinalizationRequest,
559
+ LocalArtifactFinalizationResult,
560
+ LocalArtifactFinalizationIntent,
561
+ LocalArtifactValidationOptions,
562
+ LocalArtifactValidationResult
563
+ };