@fitlab-ai/agent-infra 0.12.0 → 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.
- package/dist/lib/internal/manual-validation.js +11 -17
- package/dist/lib/internal/platform-comment.js +56 -7
- package/dist/lib/platform/issue-comments.js +373 -267
- package/dist/lib/platform/issue-summary-content.js +112 -0
- package/dist/lib/platform/pr-change-report.js +175 -26
- package/dist/lib/platform/pr-summary-content.js +54 -0
- package/dist/lib/platform/pr-summary.js +318 -284
- package/dist/lib/platform/summary-content-pipeline.js +18 -0
- package/dist/lib/sandbox/removal.js +59 -2
- package/dist/lib/task/finalization.js +16 -7
- package/dist/lib/task/platform-operation-recovery.js +87 -87
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/manual-validation.ts +12 -18
- package/lib/internal/platform-comment.ts +46 -4
- package/lib/platform/issue-comments.ts +478 -280
- package/lib/platform/issue-summary-content.ts +132 -0
- package/lib/platform/pr-change-report.ts +165 -20
- package/lib/platform/pr-summary-content.ts +60 -0
- package/lib/platform/pr-summary.ts +364 -257
- package/lib/platform/summary-content-pipeline.ts +23 -0
- package/lib/sandbox/removal.ts +57 -1
- package/lib/task/finalization.ts +11 -6
- package/lib/task/platform-operation-recovery.ts +70 -81
- package/package.json +1 -1
- package/templates/.agents/rules/pr-sync.en.md +4 -2
- package/templates/.agents/rules/pr-sync.zh-CN.md +6 -9
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -25
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -26
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -2
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +0 -18
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +0 -18
- package/templates/.agents/skills/watch-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +3 -3
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +3 -3
|
@@ -6,16 +6,19 @@ import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
|
|
|
6
6
|
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
7
7
|
import { readPrDeliveryFact } from '../task/pr-delivery-fact.js';
|
|
8
8
|
import { resolvePlatformProviderContext } from './context.js';
|
|
9
|
-
import {
|
|
9
|
+
import { inspectPlatformPullRequest } from './pull-requests.js';
|
|
10
|
+
import { normalizeCommentContent, syncCommentPlan } from './issue-comments.js';
|
|
10
11
|
import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
|
|
11
12
|
import { platformResult } from './types.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { resourceIdentityEquals } from './resource-identity.js';
|
|
13
|
+
import { providerError, providerOperationContext, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
|
|
14
|
+
import { parseResourceIdentity, resourceIdentityEquals } from './resource-identity.js';
|
|
15
15
|
import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
|
|
16
16
|
import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
|
|
17
17
|
import { readManualValidationCompletion } from '../task/manual-validation-completion.js';
|
|
18
18
|
import { readManualValidationTransaction } from '../task/manual-validation-transaction.js';
|
|
19
|
+
import { parsePrSummaryContent, renderPrSummaryContent } from './pr-summary-content.js';
|
|
20
|
+
import { runSummaryContentPipeline } from './summary-content-pipeline.js';
|
|
21
|
+
import { parseLedgerDocument } from '../task/ledger.js';
|
|
19
22
|
function summaryMarker(taskId) {
|
|
20
23
|
return `<!-- sync-pr:${taskId}:summary -->`;
|
|
21
24
|
}
|
|
@@ -58,17 +61,6 @@ function platformError(error) {
|
|
|
58
61
|
function buildPullRequestSummary(taskId, body, headSha) {
|
|
59
62
|
return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
|
|
60
63
|
}
|
|
61
|
-
function reconcileSummaryComment(comments, taskId, desired) {
|
|
62
|
-
const marker = summaryMarker(taskId);
|
|
63
|
-
const matches = comments.filter((comment) => comment.body.includes(marker));
|
|
64
|
-
if (matches.length > 1)
|
|
65
|
-
return { action: 'conflict', commentId: null };
|
|
66
|
-
if (matches.length === 0)
|
|
67
|
-
return { action: 'create', commentId: null };
|
|
68
|
-
return normalizeCommentContent(matches[0].body) === normalizeCommentContent(desired)
|
|
69
|
-
? { action: 'no-op', commentId: matches[0].id }
|
|
70
|
-
: { action: 'update', commentId: matches[0].id };
|
|
71
|
-
}
|
|
72
64
|
function canonicalArtifacts(taskDir) {
|
|
73
65
|
const allowed = /^(plan|review-plan|code|review-code|manual-validation)(?:-r\d+)?\.md$/;
|
|
74
66
|
const byFamily = new Map();
|
|
@@ -102,6 +94,247 @@ function isHardSummaryFailure(error) {
|
|
|
102
94
|
function inspectionError(error) {
|
|
103
95
|
return error.retryable ? 'blocked' : 'failed';
|
|
104
96
|
}
|
|
97
|
+
function preparationError(code, message) {
|
|
98
|
+
return Object.assign(new Error(message), { code });
|
|
99
|
+
}
|
|
100
|
+
function validateManualSummaryPhase(manual, rendered) {
|
|
101
|
+
const hasFinal = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(rendered);
|
|
102
|
+
if (hasFinal && (!manual || manual.phase !== 'final'))
|
|
103
|
+
throw preparationError('MANUAL_VALIDATION_TRANSACTION_REQUIRED', 'final manual-validation summary requires the transaction coordinator');
|
|
104
|
+
if (manual && (manual.phase === 'pending' ? hasFinal : !hasFinal))
|
|
105
|
+
throw preparationError('MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', 'manual-validation summary phase does not match the requested writer phase');
|
|
106
|
+
}
|
|
107
|
+
function readPendingManualEvidence(resolved, manual, current, prNumber) {
|
|
108
|
+
const transaction = readManualValidationTransaction(resolved.taskDir, { transactionId: manual.transactionId, taskId: resolved.taskId, prNumber, prHeadSha: current.head.sha });
|
|
109
|
+
if (!transaction.ok)
|
|
110
|
+
throw preparationError(transaction.error.code, transaction.error.message);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function readFinalManualEvidence(resolved, manual, rendered, current, prNumber) {
|
|
114
|
+
if (!manual.receiptDigest || manual.prHeadSha !== current.head.sha || manual.authority !== 'coordinator')
|
|
115
|
+
throw preparationError('MANUAL_VALIDATION_TRANSACTION_REQUIRED', 'final manual-validation summary requires transaction, receipt, and current head identity');
|
|
116
|
+
const completion = readManualValidationCompletion(resolved.taskDir, { transactionId: manual.transactionId, taskId: resolved.taskId, prNumber, prHeadSha: current.head.sha, receiptDigest: manual.receiptDigest });
|
|
117
|
+
if (!completion.ok)
|
|
118
|
+
throw preparationError(completion.error.code, completion.error.message);
|
|
119
|
+
if (completion.value.transaction.phase !== 'final-promotion-in-progress' || !completion.value.transaction.eventAppended
|
|
120
|
+
|| !manualValidationFinalSummaryProjectionMatches(rendered, completion.value.receipt))
|
|
121
|
+
throw preparationError('MANUAL_VALIDATION_SUMMARY_PROJECTION_INVALID', 'final summary does not match the receipt-backed promotion intent');
|
|
122
|
+
return completion.value.receipt.evidenceDigest;
|
|
123
|
+
}
|
|
124
|
+
async function manualValidationEvidence(resolved, manual, rendered, current) {
|
|
125
|
+
validateManualSummaryPhase(manual, rendered);
|
|
126
|
+
if (!manual)
|
|
127
|
+
return null;
|
|
128
|
+
const prNumber = current.number;
|
|
129
|
+
if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
|
|
130
|
+
throw preparationError('MANUAL_VALIDATION_PR_NUMBER_REQUIRED', 'manual-validation summary requires a positive safe pull-request number from the authoritative snapshot');
|
|
131
|
+
if (!manual.transactionId)
|
|
132
|
+
throw preparationError('MANUAL_VALIDATION_TRANSACTION_REQUIRED', 'manual-validation summary requires a transaction ID');
|
|
133
|
+
return manual.phase === 'pending'
|
|
134
|
+
? readPendingManualEvidence(resolved, manual, current, prNumber)
|
|
135
|
+
: readFinalManualEvidence(resolved, manual, rendered, current, prNumber);
|
|
136
|
+
}
|
|
137
|
+
function validatedSummaryReport(resolved, options, current, taskContent) {
|
|
138
|
+
if (!options.changeReportFile)
|
|
139
|
+
throw preparationError('PR_CHANGE_REPORT_MISSING', 'summary-sync requires --change-report-file');
|
|
140
|
+
const reportPath = taskReportPath(resolved.taskDir);
|
|
141
|
+
if (path.resolve(resolved.repoRoot, options.changeReportFile) !== reportPath)
|
|
142
|
+
throw preparationError('PR_CHANGE_REPORT_PATH_INVALID', 'summary-sync must consume the task-bound pr-change-report.json');
|
|
143
|
+
const report = readPrChangeReport(reportPath);
|
|
144
|
+
if (!report.ok)
|
|
145
|
+
throw preparationError(report.error.code, report.error.message);
|
|
146
|
+
const checked = currentReportCheck(report.value, current, taskContent, resolved.repoRoot);
|
|
147
|
+
if (!checked.ok)
|
|
148
|
+
throw preparationError(checked.error.code, checked.error.message);
|
|
149
|
+
return report.value;
|
|
150
|
+
}
|
|
151
|
+
function renderedSummaryBody(taskId, rendered, report, manual, evidenceDigest, headSha) {
|
|
152
|
+
const split = splitDocumentPlaceholder(rendered, CANONICAL_REPORT_PLACEHOLDER);
|
|
153
|
+
if (!split.ok)
|
|
154
|
+
throw preparationError('PR_SUMMARY_BODY_CONTRACT_INVALID', split.error.message);
|
|
155
|
+
const prefix = sanitizeMarkdownDocument(split.value.prefix, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
|
|
156
|
+
const suffix = sanitizeMarkdownDocument(split.value.suffix, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
|
|
157
|
+
if (!prefix.ok || !suffix.ok)
|
|
158
|
+
throw preparationError('PR_SUMMARY_BODY_CONTRACT_INVALID', 'PR summary body contract is invalid');
|
|
159
|
+
const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${CANONICAL_REPORT_PLACEHOLDER}${suffix.value}`, report);
|
|
160
|
+
if (!replaced.ok)
|
|
161
|
+
throw preparationError(replaced.error.code, replaced.error.message);
|
|
162
|
+
const withReceipt = manual?.phase === 'final' ? replaced.value.replace(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${evidenceDigest}; head=${manual.prHeadSha} -->`) : replaced.value;
|
|
163
|
+
const body = buildPullRequestSummary(taskId, withReceipt, headSha);
|
|
164
|
+
if (!isSafeSummaryEnvelope(body, taskId))
|
|
165
|
+
throw preparationError('PR_SUMMARY_RENDER_INVALID', 'Summary contains an invalid or duplicated reserved control marker');
|
|
166
|
+
return body;
|
|
167
|
+
}
|
|
168
|
+
function prepareBoundSummaryContent(body, taskContent) {
|
|
169
|
+
try {
|
|
170
|
+
return prepareAndRenderPrSummaryContent(JSON.parse(body), taskContent);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
throw preparationError('PR_SUMMARY_CONTENT_INVALID', error instanceof Error ? error.message : String(error));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function prepareBoundPullRequestSummary(resolved, options, context, loaded, expectedIdentity) {
|
|
177
|
+
const taskSnapshot = readBoundTaskSnapshot(resolved.taskMdPath);
|
|
178
|
+
if (taskSnapshot.factError)
|
|
179
|
+
throw preparationError(taskSnapshot.factError.code, taskSnapshot.factError.message);
|
|
180
|
+
if (!taskSnapshot.boundFact || !resourceIdentityEquals(taskSnapshot.boundFact.identity.resource, expectedIdentity))
|
|
181
|
+
throw preparationError('PLATFORM_OPERATION_IDENTITY_MISMATCH', 'Bound pull request identity changed while the summary was queued');
|
|
182
|
+
const preparedContent = prepareBoundSummaryContent(options.body, taskSnapshot.taskContent);
|
|
183
|
+
const current = await inspectBoundPullRequest(context, resolved.repoRoot, expectedIdentity, loaded);
|
|
184
|
+
if (!current.ok)
|
|
185
|
+
throw preparationError(current.error?.code || 'PR_SNAPSHOT_UNAVAILABLE', current.error?.message || 'Pull-request snapshot is unavailable');
|
|
186
|
+
const evidenceDigest = await manualValidationEvidence(resolved, options.manualValidation, preparedContent.rendered, current.value);
|
|
187
|
+
const report = validatedSummaryReport(resolved, options, current.value, taskSnapshot.taskContent);
|
|
188
|
+
const body = renderedSummaryBody(resolved.taskId, preparedContent.rendered, report, options.manualValidation, evidenceDigest, current.value.head.sha);
|
|
189
|
+
return {
|
|
190
|
+
parent: expectedIdentity,
|
|
191
|
+
item: { marker: summaryMarker(resolved.taskId), body, content: options.body, part: 1, total: 1 },
|
|
192
|
+
snapshot: current.value,
|
|
193
|
+
rendered: body,
|
|
194
|
+
manual: options.manualValidation,
|
|
195
|
+
prNumber: current.value.number,
|
|
196
|
+
precheckVerdict: report.precheck.verdict,
|
|
197
|
+
nextAction: report.precheck.route
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async function validateBoundPullRequestSummary(resolved, options, context, loaded, prepared) {
|
|
201
|
+
const latest = await inspectBoundPullRequest(context, resolved.repoRoot, prepared.parent, loaded);
|
|
202
|
+
if (!latest.ok)
|
|
203
|
+
return latest.error;
|
|
204
|
+
if (!sameIdentity(prepared.snapshot, latest.value))
|
|
205
|
+
return { code: 'PR_SUMMARY_HEAD_RACE', message: 'Pull-request identity or base/head snapshot changed during summary publication', retryable: true };
|
|
206
|
+
const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
|
|
207
|
+
const bound = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
208
|
+
if (bound.status !== 'valid' || bound.fact.state !== 'bound' || !resourceIdentityEquals(bound.fact.identity.resource, prepared.parent))
|
|
209
|
+
return { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound pull request identity changed during summary publication', retryable: true };
|
|
210
|
+
try {
|
|
211
|
+
await manualValidationEvidence(resolved, prepared.manual, prepared.rendered, latest.value);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const value = error;
|
|
215
|
+
return { code: value.code || 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: value.message || String(error), retryable: false };
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
async function syncBoundPullRequestSummary(resolved, options, context, loaded, expectedIdentity, preserveFailure) {
|
|
220
|
+
const state = { prepared: null };
|
|
221
|
+
const expectedDigest = createHash('sha256').update(options.body).digest('hex');
|
|
222
|
+
const operation = {
|
|
223
|
+
taskRef: resolved.taskId, cwd: resolved.repoRoot, kind: 'pull-request-summary',
|
|
224
|
+
target: JSON.stringify(expectedIdentity), expectedDigest, dependency: 'deferred',
|
|
225
|
+
pullRequestSummary: {
|
|
226
|
+
body: options.body, changeReportFile: options.changeReportFile ?? taskReportPath(resolved.taskDir),
|
|
227
|
+
...(options.manualValidation?.transactionId ? { manualValidation: { ...options.manualValidation, transactionId: options.manualValidation.transactionId } } : {})
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const strategy = {
|
|
231
|
+
taskId: resolved.taskId, cwd: resolved.repoRoot, agent: options.agent, client: options.client, resolved: { loaded, context },
|
|
232
|
+
parentKind: 'pull-request', kind: 'summary', markerPrefix: summaryMarker(resolved.taskId).replace(/ -->$/u, ''),
|
|
233
|
+
dryRun: options.dryRun, verifyReadback: true,
|
|
234
|
+
prepare: async () => {
|
|
235
|
+
const prepared = await prepareBoundPullRequestSummary(resolved, options, context, loaded, expectedIdentity);
|
|
236
|
+
state.prepared = prepared;
|
|
237
|
+
return { parent: prepared.parent, items: [prepared.item] };
|
|
238
|
+
},
|
|
239
|
+
validate: async () => state.prepared ? validateBoundPullRequestSummary(resolved, options, context, loaded, state.prepared) : null
|
|
240
|
+
};
|
|
241
|
+
const output = await syncCommentPlan(strategy, operation, options.queuedOperation);
|
|
242
|
+
if (output.error || output.status === 'failed' || output.status === 'blocked')
|
|
243
|
+
return preserveFailure({
|
|
244
|
+
...output, resource: { kind: 'pull-request', number: state.prepared?.prNumber ?? resourceIdentityNumber(expectedIdentity), identity: expectedIdentity }
|
|
245
|
+
}, state.prepared?.prNumber ?? resourceIdentityNumber(expectedIdentity));
|
|
246
|
+
const info = state.prepared
|
|
247
|
+
? { precheckVerdict: state.prepared.precheckVerdict, nextAction: state.prepared.nextAction }
|
|
248
|
+
: { precheckVerdict: 'needs-review', nextAction: 'review-code' };
|
|
249
|
+
return {
|
|
250
|
+
...output,
|
|
251
|
+
resource: { kind: 'pull-request', number: state.prepared?.prNumber ?? resourceIdentityNumber(expectedIdentity), identity: expectedIdentity },
|
|
252
|
+
comment: output.comment ? { ...output.comment, kind: 'summary' } : null,
|
|
253
|
+
result: null, warnings: [], ...info
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function validateUnboundPrSummaryInput(options, taskContent, knownPrNumber) {
|
|
257
|
+
try {
|
|
258
|
+
prepareAndRenderPrSummaryContent(JSON.parse(options.body), taskContent);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
return platformResult('failed', {
|
|
263
|
+
resource: { kind: 'pull-request', number: knownPrNumber },
|
|
264
|
+
error: { code: 'PR_SUMMARY_CONTENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function boundSummaryPhase(raw) {
|
|
269
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
270
|
+
return null;
|
|
271
|
+
const input = raw;
|
|
272
|
+
const manual = input.manualValidation && typeof input.manualValidation === 'object' && !Array.isArray(input.manualValidation)
|
|
273
|
+
? input.manualValidation : {};
|
|
274
|
+
const state = typeof manual.state === 'string' ? manual.state : null;
|
|
275
|
+
const overview = typeof input.overview === 'string' ? input.overview : '';
|
|
276
|
+
return {
|
|
277
|
+
passed: state === 'passed' || /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(overview),
|
|
278
|
+
known: state !== null || overview.length > 0
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function manualSummaryPhaseFailure(options, phase) {
|
|
282
|
+
const manual = options.manualValidation;
|
|
283
|
+
if (phase.passed && (manual?.phase !== 'final' || manual.authority !== 'coordinator'))
|
|
284
|
+
return {
|
|
285
|
+
code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator'
|
|
286
|
+
};
|
|
287
|
+
if (!manual || !phase.known)
|
|
288
|
+
return null;
|
|
289
|
+
if ((manual.phase === 'pending' && phase.passed) || (manual.phase === 'final' && !phase.passed))
|
|
290
|
+
return {
|
|
291
|
+
code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase'
|
|
292
|
+
};
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
function boundSummaryPhaseError(options, raw, knownPrNumber) {
|
|
296
|
+
const phase = boundSummaryPhase(raw);
|
|
297
|
+
if (!phase)
|
|
298
|
+
return null;
|
|
299
|
+
const error = manualSummaryPhaseFailure(options, phase);
|
|
300
|
+
return error ? platformResult('failed', { resource: { kind: 'pull-request', number: knownPrNumber }, error: { ...error, retryable: false } }) : null;
|
|
301
|
+
}
|
|
302
|
+
function validateBoundPrSummaryInput(options, knownPrNumber) {
|
|
303
|
+
try {
|
|
304
|
+
return boundSummaryPhaseError(options, JSON.parse(options.body), knownPrNumber);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
return platformResult('failed', {
|
|
308
|
+
resource: { kind: 'pull-request', number: knownPrNumber },
|
|
309
|
+
error: { code: 'PR_SUMMARY_CONTENT_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function prepareSummarySync(taskRef, options) {
|
|
314
|
+
const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
|
|
315
|
+
if (!resolved.ok)
|
|
316
|
+
return { ok: false, knownPrNumber: null, error: platformResult('failed', { error: { code: resolved.code, message: resolved.message, retryable: false } }) };
|
|
317
|
+
const knownPrNumber = boundPrNumberForWarning(resolved.taskMdPath);
|
|
318
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
319
|
+
const fact = readPrDeliveryFact(parseTypedTaskFrontmatter(taskContent), options.runtimeVersion);
|
|
320
|
+
if (fact.status === 'invalid')
|
|
321
|
+
return { ok: false, knownPrNumber, error: platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }) };
|
|
322
|
+
const isBound = fact.status === 'valid' && fact.fact.state === 'bound';
|
|
323
|
+
const contentError = isBound
|
|
324
|
+
? validateBoundPrSummaryInput(options, knownPrNumber)
|
|
325
|
+
: validateUnboundPrSummaryInput(options, taskContent, knownPrNumber);
|
|
326
|
+
if (contentError)
|
|
327
|
+
return { ok: false, knownPrNumber, error: contentError };
|
|
328
|
+
if (!options.changeReportFile && (options.dryRun || fact.status !== 'valid' || fact.fact.state !== 'bound'))
|
|
329
|
+
return {
|
|
330
|
+
ok: false, knownPrNumber, error: platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_MISSING', message: 'summary-sync requires --change-report-file', retryable: false } })
|
|
331
|
+
};
|
|
332
|
+
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
333
|
+
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
334
|
+
if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status))
|
|
335
|
+
return { ok: false, knownPrNumber, error: context };
|
|
336
|
+
return { ok: true, resolved, fact, knownPrNumber, loaded: loaded.value, context };
|
|
337
|
+
}
|
|
105
338
|
async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedContext) {
|
|
106
339
|
if (!context.platform.repository) {
|
|
107
340
|
return { ok: false, status: context.status, error: context.error };
|
|
@@ -441,10 +674,10 @@ async function reportWrite(taskRef, options) {
|
|
|
441
674
|
async function syncPullRequestSummary(taskRef, options) {
|
|
442
675
|
const warningResult = warningResultForPrimary(options.primaryResult);
|
|
443
676
|
let knownPrNumber = null;
|
|
444
|
-
const softenFailure = (output) => {
|
|
677
|
+
const softenFailure = (output, fallbackNumber = knownPrNumber) => {
|
|
445
678
|
const prNumber = output.resource.kind === 'pull-request' && output.resource.number
|
|
446
679
|
? output.resource.number
|
|
447
|
-
:
|
|
680
|
+
: fallbackNumber;
|
|
448
681
|
const warning = output.error && prNumber && output.error.code !== 'PR_NOT_LINKED'
|
|
449
682
|
? {
|
|
450
683
|
code: output.error.code,
|
|
@@ -467,277 +700,78 @@ async function syncPullRequestSummary(taskRef, options) {
|
|
|
467
700
|
}
|
|
468
701
|
: { ...output, result: null, warnings: [] };
|
|
469
702
|
};
|
|
470
|
-
const preserveFailure = (output) => options.strict
|
|
703
|
+
const preserveFailure = (output, fallbackNumber = knownPrNumber) => options.strict
|
|
704
|
+
|| isHardSummaryFailure(output.error)
|
|
471
705
|
? { ...output, result: null, warnings: [] }
|
|
472
|
-
: softenFailure(output);
|
|
473
|
-
const
|
|
474
|
-
if (!
|
|
475
|
-
return
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
: { ...softenFailure(output) };
|
|
481
|
-
};
|
|
482
|
-
const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(resolved.taskMdPath, 'utf8'));
|
|
483
|
-
const fact = readPrDeliveryFact(frontmatter, options.runtimeVersion);
|
|
484
|
-
if (fact.status === 'invalid')
|
|
485
|
-
return preserveFailure(platformResult('failed', { error: { code: fact.error.code, message: fact.error.message, retryable: false } }));
|
|
486
|
-
if (!options.changeReportFile)
|
|
487
|
-
return preserveFailure(platformResult('failed', { error: { code: 'PR_CHANGE_REPORT_MISSING', message: 'summary-sync requires --change-report-file', retryable: false } }));
|
|
488
|
-
const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
|
|
489
|
-
const context = loaded.ok ? loaded.value.context : loaded.context;
|
|
490
|
-
if (!loaded.ok || !context.platform.repository || !['no-op', 'degraded'].includes(context.status)) {
|
|
491
|
-
// Preserve the known PR target for diagnostics when provider context resolution fails.
|
|
492
|
-
knownPrNumber = boundPrNumberForWarning(resolved.taskMdPath);
|
|
493
|
-
return preserveFailure(context);
|
|
706
|
+
: softenFailure(output, fallbackNumber);
|
|
707
|
+
const prepared = await prepareSummarySync(taskRef, options);
|
|
708
|
+
if (!prepared.ok)
|
|
709
|
+
return preserveFailure(prepared.error, prepared.knownPrNumber);
|
|
710
|
+
const { resolved, fact, loaded, context } = prepared;
|
|
711
|
+
knownPrNumber = prepared.knownPrNumber;
|
|
712
|
+
if (fact.status === 'valid' && fact.fact.state === 'bound') {
|
|
713
|
+
return syncBoundPullRequestSummary(resolved, options, context, loaded, fact.fact.identity.resource, preserveFailure);
|
|
494
714
|
}
|
|
715
|
+
return preserveFailure(platformResult(options.dryRun ? 'blocked' : 'failed', { error: {
|
|
716
|
+
code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false
|
|
717
|
+
} }));
|
|
718
|
+
}
|
|
719
|
+
async function replayPullRequestSummary(taskId, operation, options) {
|
|
720
|
+
const intent = operation.pullRequestSummary;
|
|
721
|
+
if (!intent)
|
|
722
|
+
return platformResult('blocked', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary content is missing', retryable: false } });
|
|
723
|
+
let target;
|
|
495
724
|
try {
|
|
496
|
-
|
|
497
|
-
const { taskContent, boundFact, factError } = readBoundTaskSnapshot(resolved.taskMdPath);
|
|
498
|
-
if (factError)
|
|
499
|
-
return {
|
|
500
|
-
...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: factError.code, message: factError.message, retryable: false } }),
|
|
501
|
-
result: null, warnings: []
|
|
502
|
-
};
|
|
503
|
-
if (!boundFact)
|
|
504
|
-
return {
|
|
505
|
-
...platformResult('failed', { resource: { kind: 'pull-request', number: null }, error: { code: 'PR_NOT_LINKED', message: 'Task has no verified bound pull request', retryable: false } }),
|
|
506
|
-
result: null, warnings: []
|
|
507
|
-
};
|
|
508
|
-
const prIdentity = boundFact.identity.resource;
|
|
509
|
-
const boundPrNumber = resourceIdentityNumber(prIdentity);
|
|
510
|
-
knownPrNumber = boundPrNumber;
|
|
511
|
-
let prNumber = boundPrNumber;
|
|
512
|
-
const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
513
|
-
if (!initial.ok)
|
|
514
|
-
return fail(initial.status, context, initial.error);
|
|
515
|
-
const manual = options.manualValidation;
|
|
516
|
-
let manualEvidenceDigest = null;
|
|
517
|
-
const hasFinalManualValidation = /###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)/u.test(options.body);
|
|
518
|
-
if (hasFinalManualValidation && (!manual || manual.phase !== 'final'))
|
|
519
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires the transaction coordinator', retryable: false }, prNumber);
|
|
520
|
-
if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation))
|
|
521
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
|
|
522
|
-
if (manual) {
|
|
523
|
-
if (!Number.isSafeInteger(initial.value.number) || initial.value.number <= 0)
|
|
524
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a positive safe pull-request number from the authoritative snapshot', retryable: false }, prNumber);
|
|
525
|
-
prNumber = initial.value.number;
|
|
526
|
-
knownPrNumber = prNumber;
|
|
527
|
-
if (!manual.transactionId)
|
|
528
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'manual-validation summary requires a transaction ID', retryable: false }, prNumber);
|
|
529
|
-
if (manual.phase === 'pending') {
|
|
530
|
-
const transaction = readManualValidationTransaction(resolved.taskDir, {
|
|
531
|
-
transactionId: manual.transactionId,
|
|
532
|
-
taskId: resolved.taskId,
|
|
533
|
-
prNumber,
|
|
534
|
-
prHeadSha: initial.value.head.sha
|
|
535
|
-
});
|
|
536
|
-
if (!transaction.ok)
|
|
537
|
-
return fail('failed', context, platformError(transaction.error), prNumber);
|
|
538
|
-
}
|
|
539
|
-
if (manual.phase === 'final' && (!manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha))
|
|
540
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
|
|
541
|
-
if (manual.phase === 'final') {
|
|
542
|
-
if (manual.authority !== 'coordinator')
|
|
543
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires coordinator authority', retryable: false }, prNumber);
|
|
544
|
-
const completion = readManualValidationCompletion(resolved.taskDir, {
|
|
545
|
-
transactionId: manual.transactionId,
|
|
546
|
-
taskId: resolved.taskId,
|
|
547
|
-
prNumber,
|
|
548
|
-
prHeadSha: initial.value.head.sha,
|
|
549
|
-
receiptDigest: manual.receiptDigest
|
|
550
|
-
});
|
|
551
|
-
if (!completion.ok)
|
|
552
|
-
return fail('failed', context, platformError(completion.error), prNumber);
|
|
553
|
-
const { receipt, transaction } = completion.value;
|
|
554
|
-
manualEvidenceDigest = receipt.evidenceDigest;
|
|
555
|
-
if (transaction.phase !== 'final-promotion-in-progress' || !transaction.eventAppended)
|
|
556
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_PHASE_INVALID', message: 'final summary requires a receipt-backed promotion intent', retryable: false }, prNumber);
|
|
557
|
-
if (!manualValidationFinalSummaryProjectionMatches(options.body, receipt))
|
|
558
|
-
return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PROJECTION_INVALID', message: 'final summary body does not match the canonical receipt projection', retryable: false }, prNumber);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
const expectedReportPath = taskReportPath(resolved.taskDir);
|
|
562
|
-
const suppliedReportPath = path.resolve(resolved.repoRoot, options.changeReportFile);
|
|
563
|
-
if (suppliedReportPath !== expectedReportPath)
|
|
564
|
-
return fail('failed', context, { code: 'PR_CHANGE_REPORT_PATH_INVALID', message: 'summary-sync must consume the task-bound pr-change-report.json', retryable: false });
|
|
565
|
-
const report = readPrChangeReport(suppliedReportPath);
|
|
566
|
-
if (!report.ok)
|
|
567
|
-
return fail('failed', context, platformError(report.error));
|
|
568
|
-
const checked = currentReportCheck(report.value, initial.value, taskContent, resolved.repoRoot);
|
|
569
|
-
if (!checked.ok)
|
|
570
|
-
return fail('failed', context, checked.error);
|
|
571
|
-
const placeholder = CANONICAL_REPORT_PLACEHOLDER;
|
|
572
|
-
const split = splitDocumentPlaceholder(options.body, placeholder);
|
|
573
|
-
if (!split.ok)
|
|
574
|
-
return fail('failed', context, {
|
|
575
|
-
code: 'PR_SUMMARY_BODY_CONTRACT_INVALID',
|
|
576
|
-
message: split.error.message,
|
|
577
|
-
retryable: false
|
|
578
|
-
});
|
|
579
|
-
const prefix = sanitizeMarkdownDocument(split.value.prefix, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
|
|
580
|
-
const suffix = sanitizeMarkdownDocument(split.value.suffix, { reservedMarkers: [CONTROL_MARKER_PATTERN] });
|
|
581
|
-
if (!prefix.ok) {
|
|
582
|
-
return fail('failed', context, {
|
|
583
|
-
code: 'PR_SUMMARY_BODY_CONTRACT_INVALID',
|
|
584
|
-
message: `${prefix.error.message} at offset ${prefix.error.offset}`,
|
|
585
|
-
retryable: false
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
if (!suffix.ok) {
|
|
589
|
-
return fail('failed', context, {
|
|
590
|
-
code: 'PR_SUMMARY_BODY_CONTRACT_INVALID',
|
|
591
|
-
message: `${suffix.error.message} at offset ${suffix.error.offset}`,
|
|
592
|
-
retryable: false
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
const replaced = replaceCanonicalReportPlaceholder(`${prefix.value}${placeholder}${suffix.value}`, report.value);
|
|
596
|
-
if (!replaced.ok)
|
|
597
|
-
return fail('failed', context, platformError(replaced.error));
|
|
598
|
-
const renderedBody = manual?.phase === 'final'
|
|
599
|
-
? replaced.value.replace(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`)
|
|
600
|
-
: replaced.value;
|
|
601
|
-
const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
|
|
602
|
-
if (!isSafeSummaryEnvelope(desired, resolved.taskId))
|
|
603
|
-
return fail('failed', context, {
|
|
604
|
-
code: 'PR_SUMMARY_RENDER_INVALID',
|
|
605
|
-
message: 'Summary contains an invalid or duplicated reserved control marker',
|
|
606
|
-
retryable: false
|
|
607
|
-
});
|
|
608
|
-
const listed = loaded.value.provider.comments?.list
|
|
609
|
-
? await loaded.value.provider.comments.list({ context: providerOperationContext(loaded.value), parent: prIdentity }).then((response) => response.ok
|
|
610
|
-
? { ok: true, value: response.value.map((comment) => ({ id: /^\d+$/.test(comment.id) ? Number(comment.id) : comment.id, body: comment.body })) }
|
|
611
|
-
: response)
|
|
612
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.list');
|
|
613
|
-
if (!listed.ok)
|
|
614
|
-
return fail(providerStatus(listed.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(listed.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'));
|
|
615
|
-
const reconciliation = reconcileSummaryComment(listed.value, resolved.taskId, desired);
|
|
616
|
-
if (reconciliation.action === 'conflict')
|
|
617
|
-
return fail('failed', context, { code: 'PR_SUMMARY_MARKER_AMBIGUOUS', message: 'Multiple PR comments contain the summary marker', retryable: false });
|
|
618
|
-
const previousBody = reconciliation.action === 'update'
|
|
619
|
-
? listed.value.find((comment) => comment.id === reconciliation.commentId)?.body ?? null
|
|
620
|
-
: null;
|
|
621
|
-
const info = { precheckVerdict: report.value.precheck.verdict, nextAction: report.value.precheck.route };
|
|
622
|
-
const beforeWrite = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
623
|
-
if (!beforeWrite.ok)
|
|
624
|
-
return fail(beforeWrite.status, context, beforeWrite.error, prNumber);
|
|
625
|
-
if (!sameIdentity(initial.value, beforeWrite.value))
|
|
626
|
-
return fail('blocked', context, {
|
|
627
|
-
code: 'PR_SUMMARY_HEAD_RACE',
|
|
628
|
-
message: 'Pull request head changed before publishing the summary',
|
|
629
|
-
retryable: true
|
|
630
|
-
}, prNumber);
|
|
631
|
-
if (reconciliation.action === 'no-op')
|
|
632
|
-
return {
|
|
633
|
-
...basePlatformResult('no-op', context, resolved.taskId, prNumber),
|
|
634
|
-
comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: [reconciliation.commentId], parts: 1 },
|
|
635
|
-
result: null, warnings: [], ...info
|
|
636
|
-
};
|
|
637
|
-
if (options.dryRun)
|
|
638
|
-
return {
|
|
639
|
-
...basePlatformResult('planned', context, resolved.taskId, prNumber),
|
|
640
|
-
operations: [{ name: `summary:${reconciliation.action}`, status: 'planned', reasonCode: null }],
|
|
641
|
-
result: null, warnings: [], ...info
|
|
642
|
-
};
|
|
643
|
-
const publish = async () => {
|
|
644
|
-
const existingComment = reconciliation.commentId === null
|
|
645
|
-
? undefined
|
|
646
|
-
: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId));
|
|
647
|
-
const written = loaded.value.provider.comments?.write
|
|
648
|
-
? await loaded.value.provider.comments.write({
|
|
649
|
-
context: providerOperationContext(loaded.value),
|
|
650
|
-
parent: prIdentity,
|
|
651
|
-
body: desired,
|
|
652
|
-
...(existingComment ? { existingComment } : {}),
|
|
653
|
-
mutation: { idempotencyKey: `pr-summary:${resolved.taskId}` }
|
|
654
|
-
})
|
|
655
|
-
: unsupportedProviderOperation(loaded.value.provider, 'comments.write');
|
|
656
|
-
if (!written.ok)
|
|
657
|
-
return fail(providerStatus(written.error) === 'blocked' ? 'blocked' : 'failed', context, providerError(written.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), prNumber);
|
|
658
|
-
const after = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
|
|
659
|
-
const id = /^\d+$/.test(written.value.remoteId) ? Number(written.value.remoteId) : written.value.remoteId;
|
|
660
|
-
const compensateWrittenSummary = async () => reconciliation.action === 'create'
|
|
661
|
-
? written.value.remoteId && loaded.value.provider.comments?.delete
|
|
662
|
-
? await loaded.value.provider.comments.delete({
|
|
663
|
-
context: providerOperationContext(loaded.value),
|
|
664
|
-
parent: prIdentity,
|
|
665
|
-
comment: providerResourceToken(loaded.value.provider, 'comment', written.value.remoteId),
|
|
666
|
-
mutation: { idempotencyKey: `pr-summary-delete:${resolved.taskId}:${written.value.remoteId}` }
|
|
667
|
-
})
|
|
668
|
-
: { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Created summary comment has no valid id', retryable: true } }
|
|
669
|
-
: reconciliation.commentId && previousBody !== null && loaded.value.provider.comments?.write
|
|
670
|
-
? await loaded.value.provider.comments.write({
|
|
671
|
-
context: providerOperationContext(loaded.value),
|
|
672
|
-
parent: prIdentity,
|
|
673
|
-
body: previousBody,
|
|
674
|
-
existingComment: providerResourceToken(loaded.value.provider, 'comment', String(reconciliation.commentId)),
|
|
675
|
-
mutation: { idempotencyKey: `pr-summary-restore:${resolved.taskId}` }
|
|
676
|
-
})
|
|
677
|
-
: { ok: false, error: { code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED', message: 'Previous summary body is unavailable for restoration', retryable: true } };
|
|
678
|
-
if (!after.ok) {
|
|
679
|
-
const compensated = await compensateWrittenSummary();
|
|
680
|
-
if (!compensated.ok)
|
|
681
|
-
return fail('blocked', context, {
|
|
682
|
-
code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
|
|
683
|
-
message: `Summary post-write verification failed and compensation failed: ${compensated.error.message}`,
|
|
684
|
-
retryable: true
|
|
685
|
-
}, prNumber);
|
|
686
|
-
return fail('blocked', context, {
|
|
687
|
-
code: 'PR_SUMMARY_POSTWRITE_VERIFY_FAILED',
|
|
688
|
-
message: `Summary was written but pull-request verification failed: ${after.error?.message || 'unknown verification error'}; the previous summary was restored or the new comment was deleted`,
|
|
689
|
-
retryable: true
|
|
690
|
-
}, prNumber);
|
|
691
|
-
}
|
|
692
|
-
if (!sameIdentity(initial.value, after.value)) {
|
|
693
|
-
const compensated = await compensateWrittenSummary();
|
|
694
|
-
if (!compensated.ok)
|
|
695
|
-
return fail('blocked', context, {
|
|
696
|
-
code: 'PR_SUMMARY_RACE_COMPENSATION_FAILED',
|
|
697
|
-
message: `Pull-request head changed and summary compensation failed: ${compensated.error.message}`,
|
|
698
|
-
retryable: true
|
|
699
|
-
}, prNumber);
|
|
700
|
-
return fail('blocked', context, {
|
|
701
|
-
code: 'PR_SUMMARY_HEAD_RACE',
|
|
702
|
-
message: 'Pull-request head changed while publishing; the previous summary was restored or the new comment was deleted; rerun report and summary sync',
|
|
703
|
-
retryable: true
|
|
704
|
-
}, prNumber);
|
|
705
|
-
}
|
|
706
|
-
return {
|
|
707
|
-
...basePlatformResult('applied', context, resolved.taskId, prNumber),
|
|
708
|
-
comment: { kind: 'summary', marker: summaryMarker(resolved.taskId), ids: Number.isInteger(id) ? [id] : [], parts: 1 },
|
|
709
|
-
operations: [{ name: `summary:${reconciliation.action}`, status: 'applied', reasonCode: null }],
|
|
710
|
-
result: null, warnings: [], ...info
|
|
711
|
-
};
|
|
712
|
-
};
|
|
713
|
-
const operation = {
|
|
714
|
-
taskRef: resolved.taskId,
|
|
715
|
-
cwd: resolved.repoRoot,
|
|
716
|
-
kind: 'pull-request-summary',
|
|
717
|
-
target: JSON.stringify(prIdentity),
|
|
718
|
-
expectedDigest: createHash('sha256').update(desired).digest('hex'),
|
|
719
|
-
dependency: 'deferred',
|
|
720
|
-
pullRequestSummary: {
|
|
721
|
-
body: options.body,
|
|
722
|
-
changeReportFile: options.changeReportFile,
|
|
723
|
-
...(manual?.transactionId ? { manualValidation: { ...manual, transactionId: manual.transactionId } } : {})
|
|
724
|
-
}
|
|
725
|
-
};
|
|
726
|
-
if (options.skipQueue)
|
|
727
|
-
return publish();
|
|
728
|
-
return coordinatePlatformWrite({
|
|
729
|
-
operation,
|
|
730
|
-
agent: options.agent,
|
|
731
|
-
execute: publish,
|
|
732
|
-
block: (error) => fail('blocked', context, error, prNumber),
|
|
733
|
-
persistenceFailure: (error) => fail('failed', context, error, prNumber)
|
|
734
|
-
});
|
|
735
|
-
};
|
|
736
|
-
return await execute();
|
|
725
|
+
target = parseResourceIdentity(JSON.parse(operation.target), 'queued pull-request summary identity');
|
|
737
726
|
}
|
|
738
|
-
catch
|
|
739
|
-
return
|
|
727
|
+
catch {
|
|
728
|
+
return platformResult('failed', { error: { code: 'PLATFORM_OPERATION_PAYLOAD_INVALID', message: 'Queued pull-request summary identity is invalid', retryable: false } });
|
|
729
|
+
}
|
|
730
|
+
const bound = await inspectPlatformPullRequest(taskId, { cwd: options.cwd, client: options.client });
|
|
731
|
+
if (!bound.pullRequest || !resourceIdentityEquals(bound.pullRequest.identity, target))
|
|
732
|
+
return platformResult('blocked', {
|
|
733
|
+
resource: bound.pullRequest ? { kind: 'pull-request', number: bound.pullRequest.number, identity: bound.pullRequest.identity } : undefined,
|
|
734
|
+
error: bound.error ?? { code: 'PLATFORM_OPERATION_IDENTITY_MISMATCH', message: 'Bound pull request differs from the queued summary target', retryable: false }
|
|
735
|
+
});
|
|
736
|
+
const replayed = await syncPullRequestSummary(taskId, {
|
|
737
|
+
agent: options.agent, cwd: options.cwd, client: options.client, body: intent.body,
|
|
738
|
+
changeReportFile: intent.changeReportFile, ...(intent.manualValidation ? { manualValidation: intent.manualValidation } : {}),
|
|
739
|
+
primaryResult: 'no_op', strict: true, queuedOperation: operation
|
|
740
|
+
});
|
|
741
|
+
return (replayed.status === 'applied' || replayed.status === 'no-op') && !replayed.error && replayed.warnings.length === 0
|
|
742
|
+
? replayed
|
|
743
|
+
: platformResult('blocked', { resource: replayed.resource, error: replayed.error ?? {
|
|
744
|
+
code: 'PR_SUMMARY_RECOVERY_UNCONFIRMED', message: 'Pull-request summary preparation or recovery did not confirm the frozen operation', retryable: true
|
|
745
|
+
} });
|
|
746
|
+
}
|
|
747
|
+
function correctPrSummaryContent(value, taskContent) {
|
|
748
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
749
|
+
return parsePrSummaryContent(value);
|
|
750
|
+
const input = { ...value };
|
|
751
|
+
const ledger = parseLedgerDocument(taskContent);
|
|
752
|
+
if (ledger.present) {
|
|
753
|
+
const decisions = ledger.rows.filter((row) => row.id.startsWith('HD-'));
|
|
754
|
+
const open = decisions.filter((row) => row.status === 'needs-human-decision').length;
|
|
755
|
+
const resolved = decisions.filter((row) => row.status === 'human-decided').length;
|
|
756
|
+
const current = input.humanDecisions && typeof input.humanDecisions === 'object' && !Array.isArray(input.humanDecisions)
|
|
757
|
+
? { ...input.humanDecisions }
|
|
758
|
+
: {};
|
|
759
|
+
if (!Number.isSafeInteger(current.open) || Number(current.open) < 0)
|
|
760
|
+
current.open = open;
|
|
761
|
+
if (!Number.isSafeInteger(current.resolved) || Number(current.resolved) < 0)
|
|
762
|
+
current.resolved = resolved;
|
|
763
|
+
if (typeof current.detail !== 'string' || !current.detail.trim())
|
|
764
|
+
current.detail = `Task decision ledger: ${open} open, ${resolved} resolved.`;
|
|
765
|
+
input.humanDecisions = current;
|
|
740
766
|
}
|
|
767
|
+
return input;
|
|
768
|
+
}
|
|
769
|
+
export function prepareAndRenderPrSummaryContent(value, taskContent) {
|
|
770
|
+
return runSummaryContentPipeline(value, {
|
|
771
|
+
validate: parsePrSummaryContent,
|
|
772
|
+
correct: (input) => correctPrSummaryContent(input, taskContent),
|
|
773
|
+
render: renderPrSummaryContent
|
|
774
|
+
});
|
|
741
775
|
}
|
|
742
|
-
export { buildPullRequestSummary,
|
|
776
|
+
export { buildPullRequestSummary, reportWrite, summaryCommentState, summaryContext, summaryMarker, syncPullRequestSummary, replayPullRequestSummary, warningResultForPrimary };
|
|
743
777
|
//# sourceMappingURL=pr-summary.js.map
|