@fitlab-ai/agent-infra 0.9.15 → 0.11.0
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/README.md +2 -6
- package/README.zh-CN.md +2 -6
- package/bin/internal-cli.sh +0 -13
- package/dist/bin/internal-cli.js +11 -49
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +6 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +43 -3
- package/dist/lib/fs/durable-write.js +2 -0
- package/dist/lib/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/manual-validation.js +17 -8
- package/dist/lib/internal/platform-comment.js +60 -2
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-lifecycle.js +4 -4
- package/dist/lib/internal/task-operation-registry.js +22 -13
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +12 -7
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +89 -31
- package/dist/lib/platform/issues.js +1 -1
- package/dist/lib/platform/pr-change-report.js +5 -4
- package/dist/lib/platform/pr-summary.js +64 -32
- package/dist/lib/platform/pull-requests.js +3 -3
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-identities.js +2 -9
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +45 -7
- package/dist/lib/pr-review/evidence-grading.js +4 -8
- package/dist/lib/sandbox/commands/rm.js +21 -2
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/server.js +29 -9
- package/dist/lib/sandbox/control/state.js +7 -1
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +87 -35
- package/dist/lib/sandbox/index.js +1 -2
- package/dist/lib/sandbox/removal.js +116 -1011
- package/dist/lib/sandbox/workspace-identity.js +17 -1
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +11 -30
- package/dist/lib/task/artifact-operations.js +17 -231
- package/dist/lib/task/artifact-recovery.js +476 -0
- package/dist/lib/task/artifact-repair-intent.js +90 -39
- package/dist/lib/task/commit-operation.js +31 -4
- package/dist/lib/task/control-authority.js +39 -8
- package/dist/lib/task/control-recovery.js +14 -2
- package/dist/lib/task/create-service.js +1 -8
- package/dist/lib/task/delegation-receipts.js +64 -3
- package/dist/lib/task/events.js +165 -23
- package/dist/lib/task/human-override.js +5 -8
- package/dist/lib/task/intermediate-cleanup.js +91 -8
- package/dist/lib/task/lifecycle-recovery.js +549 -0
- package/dist/lib/task/lifecycle.js +12 -3
- package/dist/lib/task/local-artifact-finalization.js +212 -123
- package/dist/lib/task/manual-validation-receipt.js +5 -1
- package/dist/lib/task/orchestration.js +52 -2
- package/dist/lib/task/pr-delivery-fact.js +12 -56
- package/dist/lib/task/recovery-warning.js +19 -0
- package/dist/lib/task/review-command.js +3 -2
- package/dist/lib/task/review-finalization.js +294 -74
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/short-id.js +15 -7
- package/dist/lib/task/task-execution-lock.js +1 -1
- package/dist/lib/task/verification-engine.js +3 -14
- package/dist/lib/task/workflow-command.js +2 -1
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lib/version.js +1 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +7 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +55 -3
- package/lib/fs/durable-write.ts +1 -0
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/manual-validation.ts +17 -8
- package/lib/internal/platform-comment.ts +53 -2
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +1 -1
- package/lib/internal/task-lifecycle.ts +5 -4
- package/lib/internal/task-operation-registry.ts +22 -13
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +11 -7
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +105 -30
- package/lib/platform/issues.ts +1 -1
- package/lib/platform/pr-change-report.ts +7 -5
- package/lib/platform/pr-summary.ts +64 -32
- package/lib/platform/pull-requests.ts +3 -3
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-identities.ts +3 -14
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +48 -11
- package/lib/pr-review/evidence-grading.ts +4 -9
- package/lib/sandbox/commands/rm.ts +34 -2
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +6 -1
- package/lib/sandbox/control/server.ts +30 -10
- package/lib/sandbox/control/state.ts +9 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +106 -35
- package/lib/sandbox/index.ts +1 -2
- package/lib/sandbox/removal.ts +126 -1128
- package/lib/sandbox/workspace-identity.ts +19 -3
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +12 -29
- package/lib/task/artifact-operations.ts +22 -252
- package/lib/task/artifact-recovery.ts +572 -0
- package/lib/task/artifact-repair-intent.ts +126 -51
- package/lib/task/commit-operation.ts +35 -4
- package/lib/task/control-authority.ts +48 -15
- package/lib/task/control-recovery.ts +13 -1
- package/lib/task/create-service.ts +1 -10
- package/lib/task/delegation-receipts.ts +73 -2
- package/lib/task/events.ts +188 -24
- package/lib/task/human-override.ts +5 -8
- package/lib/task/intermediate-cleanup.ts +66 -12
- package/lib/task/lifecycle-recovery.ts +691 -0
- package/lib/task/lifecycle.ts +13 -4
- package/lib/task/local-artifact-finalization.ts +249 -131
- package/lib/task/manual-validation-receipt.ts +5 -1
- package/lib/task/orchestration.ts +65 -0
- package/lib/task/pr-delivery-fact.ts +15 -62
- package/lib/task/recovery-warning.ts +21 -0
- package/lib/task/review-command.ts +2 -1
- package/lib/task/review-finalization.ts +298 -84
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/short-id.ts +27 -5
- package/lib/task/task-execution-lock.ts +1 -1
- package/lib/task/verification-engine.ts +3 -17
- package/lib/task/verification-types.ts +0 -2
- package/lib/task/workflow-command.ts +2 -1
- package/lib/task/workflow-dispatch.ts +171 -0
- package/lib/version.ts +1 -15
- package/package.json +1 -1
- package/templates/.agents/rules/create-issue.en.md +1 -1
- package/templates/.agents/rules/create-issue.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +12 -2
- package/templates/.agents/rules/issue-sync.zh-CN.md +13 -5
- package/templates/.agents/rules/local-artifact-repair.en.md +47 -39
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +45 -36
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/config/verify.json +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +7 -7
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-pr/config/verify.json +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/create-task/config/verify.json +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +8 -8
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/import-issue/config/verify.json +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +11 -6
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +11 -6
- package/templates/.agents/skills/restore-task/config/verify.json +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +6 -6
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -4
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-code/reference/report-template.en.md +12 -12
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +12 -12
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +7 -7
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/review-pr/reference/evidence-grading.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/evidence-grading.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +1 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/import-issue.en.md +1 -1
- package/templates/.claude/commands/import-issue.zh-CN.md +1 -1
- package/templates/.claude/commands/restore-task.en.md +1 -1
- package/templates/.claude/commands/restore-task.zh-CN.md +1 -1
- package/dist/lib/host-control/audit.js +0 -19
- package/dist/lib/host-control/client.js +0 -142
- package/dist/lib/host-control/command.js +0 -49
- package/dist/lib/host-control/path.js +0 -160
- package/dist/lib/host-control/server.js +0 -159
- package/dist/lib/host-control/service.js +0 -112
- package/dist/lib/internal/host-control.js +0 -28
- package/lib/host-control/audit.ts +0 -22
- package/lib/host-control/client.ts +0 -172
- package/lib/host-control/command.ts +0 -60
- package/lib/host-control/path.ts +0 -167
- package/lib/host-control/server.ts +0 -180
- package/lib/host-control/service.ts +0 -133
- package/lib/internal/host-control.ts +0 -28
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isLegacyCompatibilityEnabled, legacyCompatibilityError, VERSION } from '../version.js';
|
|
2
1
|
import { isResourceIdentity, parseResourceIdentity, resourceIdentityNumber, serializeResourceIdentity } from '../platform/resource-identity.js';
|
|
3
2
|
const PR_DELIVERY_FACT_KEY = 'pr_delivery_fact';
|
|
4
3
|
function factError(message) {
|
|
@@ -28,11 +27,6 @@ function timestamp(value, label) {
|
|
|
28
27
|
throw factError(`${label} must be a valid timestamp`);
|
|
29
28
|
return new Date(result).toISOString();
|
|
30
29
|
}
|
|
31
|
-
function positiveNumber(value, label) {
|
|
32
|
-
if (!Number.isSafeInteger(value) || value <= 0)
|
|
33
|
-
throw factError(`${label} must be a positive integer`);
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
30
|
function parseRef(value, label) {
|
|
37
31
|
if (!isRecord(value))
|
|
38
32
|
throw factError(`${label} must be an object`);
|
|
@@ -51,36 +45,17 @@ function canonicalIdentity(value) {
|
|
|
51
45
|
base: parseRef(value.base, 'identity.base')
|
|
52
46
|
};
|
|
53
47
|
}
|
|
54
|
-
function decodeLegacyIdentity(value) {
|
|
55
|
-
if (!isRecord(value))
|
|
56
|
-
throw factError('legacy identity must be an object');
|
|
57
|
-
// TODO(compat): Remove the v1 identity decoder before the first stable v1.0.0 release.
|
|
58
|
-
exactKeys(value, ['repository', 'number', 'nodeId', 'url', 'head', 'base'], 'legacy identity');
|
|
59
|
-
const legacy = {
|
|
60
|
-
number: positiveNumber(value.number, 'legacy identity.number'),
|
|
61
|
-
repository: text(value.repository, 'identity.repository'),
|
|
62
|
-
nodeId: text(value.nodeId, 'legacy identity.nodeId'),
|
|
63
|
-
url: text(value.url, 'identity.url'),
|
|
64
|
-
head: parseRef(value.head, 'identity.head'),
|
|
65
|
-
base: parseRef(value.base, 'identity.base')
|
|
66
|
-
};
|
|
67
|
-
return {
|
|
68
|
-
resource: { kind: 'number', value: legacy.number },
|
|
69
|
-
repository: legacy.repository,
|
|
70
|
-
url: legacy.url,
|
|
71
|
-
head: legacy.head,
|
|
72
|
-
base: legacy.base
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
48
|
function provenanceForSource(source) { return source === 'created' ? 'create-post' : source === 'reused' ? 'reuse' : source; }
|
|
76
|
-
function
|
|
49
|
+
function legacyFactError() {
|
|
50
|
+
return Object.assign(new Error('PLATFORM_IDENTITY_LEGACY_UNSUPPORTED: v1 PR delivery fact is unsupported; use the previous tool version to view this task or migrate its fact to version 2 before using this version'), { code: 'PLATFORM_IDENTITY_LEGACY_UNSUPPORTED' });
|
|
51
|
+
}
|
|
52
|
+
function parseFact(value) {
|
|
77
53
|
if (!isRecord(value))
|
|
78
54
|
throw factError('fact must be an object');
|
|
79
|
-
if (
|
|
55
|
+
if (value.version === 1)
|
|
56
|
+
throw legacyFactError();
|
|
57
|
+
if (value.version !== 2 || typeof value.state !== 'string')
|
|
80
58
|
throw factError('version or state is invalid');
|
|
81
|
-
if (value.version === 1 && !isLegacyCompatibilityEnabled(runtimeVersion)) {
|
|
82
|
-
throw legacyCompatibilityError('v1 PR delivery fact', runtimeVersion);
|
|
83
|
-
}
|
|
84
59
|
if (value.state === 'unbound') {
|
|
85
60
|
exactKeys(value, ['version', 'state', 'reason'], 'unbound fact');
|
|
86
61
|
if (value.reason !== 'initial')
|
|
@@ -95,25 +70,6 @@ function parseFact(value, runtimeVersion = VERSION) {
|
|
|
95
70
|
}
|
|
96
71
|
if (value.state !== 'bound')
|
|
97
72
|
throw factError('state is invalid');
|
|
98
|
-
if (value.version === 1) {
|
|
99
|
-
// TODO(compat): Remove this v1-to-v2 shape conversion before the first stable v1.0.0 release.
|
|
100
|
-
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'legacy bound fact');
|
|
101
|
-
if (!isRecord(value.binding) || !isRecord(value.provenance))
|
|
102
|
-
throw factError('legacy binding or provenance is invalid');
|
|
103
|
-
exactKeys(value.binding, ['status', 'source', 'verifiedAt', 'issueNumber', 'remoteState', 'mergedAt', 'mergeCommitSha'], 'legacy binding');
|
|
104
|
-
const issueNumber = value.binding.issueNumber === null ? null : positiveNumber(value.binding.issueNumber, 'legacy binding.issueNumber');
|
|
105
|
-
return parseFact({
|
|
106
|
-
version: 2,
|
|
107
|
-
state: 'bound',
|
|
108
|
-
identity: decodeLegacyIdentity(value.identity),
|
|
109
|
-
binding: {
|
|
110
|
-
status: 'verified', source: value.binding.source, verifiedAt: timestamp(value.binding.verifiedAt, 'binding.verifiedAt'),
|
|
111
|
-
issueIdentity: issueNumber === null ? null : { kind: 'number', value: issueNumber }, remoteState: value.binding.remoteState,
|
|
112
|
-
mergedAt: value.binding.mergedAt === null ? null : timestamp(value.binding.mergedAt, 'binding.mergedAt'), mergeCommitSha: nullableText(value.binding.mergeCommitSha, 'binding.mergeCommitSha')
|
|
113
|
-
},
|
|
114
|
-
provenance: value.provenance
|
|
115
|
-
}, runtimeVersion);
|
|
116
|
-
}
|
|
117
73
|
exactKeys(value, ['version', 'state', 'identity', 'binding', 'provenance'], 'bound fact');
|
|
118
74
|
if (!isRecord(value.binding) || !isRecord(value.provenance))
|
|
119
75
|
throw factError('binding or provenance is invalid');
|
|
@@ -142,7 +98,7 @@ function parseFact(value, runtimeVersion = VERSION) {
|
|
|
142
98
|
provenance: { establishedBy: value.provenance.establishedBy }
|
|
143
99
|
};
|
|
144
100
|
}
|
|
145
|
-
function decodePrDeliveryFact(value,
|
|
101
|
+
function decodePrDeliveryFact(value, _runtimeVersion) {
|
|
146
102
|
if (typeof value === 'string') {
|
|
147
103
|
let decoded;
|
|
148
104
|
try {
|
|
@@ -151,16 +107,16 @@ function decodePrDeliveryFact(value, runtimeVersion = VERSION) {
|
|
|
151
107
|
catch {
|
|
152
108
|
throw factError('value is not valid JSON');
|
|
153
109
|
}
|
|
154
|
-
return parseFact(decoded
|
|
110
|
+
return parseFact(decoded);
|
|
155
111
|
}
|
|
156
|
-
return parseFact(value
|
|
112
|
+
return parseFact(value);
|
|
157
113
|
}
|
|
158
114
|
function encodePrDeliveryFact(fact) { return JSON.stringify(parseFact(fact)); }
|
|
159
|
-
function readPrDeliveryFact(metadata,
|
|
115
|
+
function readPrDeliveryFact(metadata, _runtimeVersion) {
|
|
160
116
|
if (!Object.hasOwn(metadata, PR_DELIVERY_FACT_KEY) || metadata[PR_DELIVERY_FACT_KEY] === '')
|
|
161
117
|
return { status: 'missing', fact: null };
|
|
162
118
|
try {
|
|
163
|
-
return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]
|
|
119
|
+
return { status: 'valid', fact: decodePrDeliveryFact(metadata[PR_DELIVERY_FACT_KEY]) };
|
|
164
120
|
}
|
|
165
121
|
catch (error) {
|
|
166
122
|
return { status: 'invalid', fact: null, error: asFactError(error) };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function warningText(value) {
|
|
2
|
+
return typeof value === 'string' && value.length > 0 && value.trim() === value && !/[\r\n]/u.test(value);
|
|
3
|
+
}
|
|
4
|
+
export function isRecoveryWarning(value) {
|
|
5
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
6
|
+
return false;
|
|
7
|
+
const warning = value;
|
|
8
|
+
return Object.keys(warning).sort().join(',') === 'action,code,message'
|
|
9
|
+
&& warningText(warning.code)
|
|
10
|
+
&& warningText(warning.message)
|
|
11
|
+
&& warningText(warning.action);
|
|
12
|
+
}
|
|
13
|
+
export function sameRecoveryWarning(left, right) {
|
|
14
|
+
return isRecoveryWarning(left) && isRecoveryWarning(right)
|
|
15
|
+
&& left.code === right.code
|
|
16
|
+
&& left.message === right.message
|
|
17
|
+
&& left.action === right.action;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=recovery-warning.js.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export function parseReviewCommand(args) {
|
|
2
|
-
if (!args[0] || args[1]
|
|
3
|
-
throw new Error('task ref and
|
|
2
|
+
if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? ''))
|
|
3
|
+
throw new Error('task ref and a supported review operation are required');
|
|
4
4
|
const values = {};
|
|
5
5
|
const flags = {
|
|
6
6
|
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
|
|
7
|
+
'--recovery-id': 'recoveryId',
|
|
7
8
|
'--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
|
|
8
9
|
};
|
|
9
10
|
for (let index = 2; index < args.length; index += 1) {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
2
|
import { parseArtifactName } from './artifact-name.js';
|
|
5
3
|
import { validateCompletedArtifact, hasOpenArtifactRound, validateArtifactPublication } from './artifact-lifecycle.js';
|
|
6
4
|
import { LEDGER_SECTION_MISSING_CODE, LEDGER_SECTION_MISSING_MESSAGE, parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
@@ -12,18 +10,16 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
|
|
|
12
10
|
import { allowsManualOverride } from './guard-override.js';
|
|
13
11
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
14
12
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
|
|
15
|
-
import {
|
|
13
|
+
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.js';
|
|
14
|
+
import { readArtifactRecoveryIntent } from './artifact-repair-intent.js';
|
|
15
|
+
import { beginArtifactRecovery, commitArtifactRecovery, prepareArtifactRecoveryFinal, prepareArtifactRecoveryCommit, reconcileArtifactRecovery, recoveryContextFromIntent, stageArtifactCandidate } from './artifact-recovery.js';
|
|
16
16
|
const STAGES = {
|
|
17
17
|
analysis: { family: 'review-analysis' },
|
|
18
18
|
plan: { family: 'review-plan' },
|
|
19
19
|
code: { family: 'review-code' }
|
|
20
20
|
};
|
|
21
21
|
const DEFAULT_FILE_SYSTEM = {
|
|
22
|
-
readFileSync: (file) => fs.readFileSync(file, 'utf8')
|
|
23
|
-
statModeSync: (file) => fs.statSync(file).mode,
|
|
24
|
-
writeFileSync: (file, content, mode) => fs.writeFileSync(file, content, { encoding: 'utf8', flag: 'wx', mode }),
|
|
25
|
-
renameSync: (from, to) => fs.renameSync(from, to),
|
|
26
|
-
unlinkSync: (file) => fs.unlinkSync(file)
|
|
22
|
+
readFileSync: (file) => fs.readFileSync(file, 'utf8')
|
|
27
23
|
};
|
|
28
24
|
function failed(request, code, message, taskId = null, stageStatus = null, extra = {}) {
|
|
29
25
|
return {
|
|
@@ -37,21 +33,167 @@ function failed(request, code, message, taskId = null, stageStatus = null, extra
|
|
|
37
33
|
stageStatus,
|
|
38
34
|
artifactSha256: null,
|
|
39
35
|
semanticDigest: null,
|
|
40
|
-
repairable: false,
|
|
41
|
-
operation: null,
|
|
42
36
|
operations: [],
|
|
43
37
|
error: { code, message },
|
|
44
38
|
...extra
|
|
45
39
|
};
|
|
46
40
|
}
|
|
47
|
-
function
|
|
41
|
+
function preflightFailed(request, code, message, taskId = null, extra = {}) {
|
|
42
|
+
const { status: _status, intent: _intent, stageStatus: _stageStatus, ...finalizationExtra } = extra;
|
|
43
|
+
return {
|
|
44
|
+
...failed(request, code, message, taskId, null, finalizationExtra),
|
|
45
|
+
status: 'failed',
|
|
46
|
+
intent: 'preflight',
|
|
47
|
+
stageStatus: null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Validate and seal a review draft before any finding-ledger write. */
|
|
51
|
+
function preflightReviewSummaryUnlocked(request, options = {}) {
|
|
52
|
+
const stage = request.stage;
|
|
53
|
+
const spec = STAGES[stage];
|
|
54
|
+
if (!spec)
|
|
55
|
+
return preflightFailed(request, 'REVIEW_STAGE_INVALID', `unsupported review stage '${request.stage}'`);
|
|
56
|
+
if (!request.taskRef || !request.artifact)
|
|
57
|
+
return preflightFailed(request, 'REVIEW_PAYLOAD_INVALID', 'taskRef, stage, and artifact are required');
|
|
58
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
59
|
+
if (!resolved.ok)
|
|
60
|
+
return preflightFailed(request, resolved.code, resolved.message, resolved.taskId ?? null);
|
|
61
|
+
if (resolved.state !== 'active')
|
|
62
|
+
return preflightFailed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
63
|
+
const parsed = parseArtifactName(request.artifact);
|
|
64
|
+
if (!parsed || parsed.family !== spec.family)
|
|
65
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${spec.family}`, resolved.taskId);
|
|
66
|
+
const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
|
|
67
|
+
if (publicationError)
|
|
68
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
|
|
69
|
+
let taskContent;
|
|
70
|
+
let content;
|
|
71
|
+
let recovery;
|
|
72
|
+
let existingIntent;
|
|
48
73
|
try {
|
|
49
|
-
|
|
74
|
+
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
75
|
+
if (request.recoveryId) {
|
|
76
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
77
|
+
if (!intent || intent.recoveryOperationId !== request.recoveryId)
|
|
78
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
|
|
79
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state))
|
|
80
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
|
|
81
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
82
|
+
content = fs.existsSync(recovery.stagingPath)
|
|
83
|
+
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
84
|
+
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
88
|
+
if (!validated.ok)
|
|
89
|
+
return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
90
|
+
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
91
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
92
|
+
existingIntent = intent;
|
|
93
|
+
if (intent?.state === 'preflight-ready')
|
|
94
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
95
|
+
if (intent?.state === 'consumed')
|
|
96
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
|
|
97
|
+
}
|
|
50
98
|
}
|
|
51
|
-
catch {
|
|
52
|
-
|
|
99
|
+
catch (error) {
|
|
100
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId);
|
|
101
|
+
}
|
|
102
|
+
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round))
|
|
103
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
|
|
104
|
+
const execution = validateLifecycleExecution(request.taskRef, {
|
|
105
|
+
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
106
|
+
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
107
|
+
dryRun: request.dryRun
|
|
108
|
+
}, { repoRoot: options.repoRoot });
|
|
109
|
+
if (!execution.ok)
|
|
110
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${execution.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH'}: ${execution.error?.message ?? 'orchestration provenance validation failed'}`, resolved.taskId);
|
|
111
|
+
const artifactSha256 = sha256Content(content);
|
|
112
|
+
const semanticDigest = canonicalSemanticDigest(content);
|
|
113
|
+
const detail = inspectDecisionDetailDuplicates(content);
|
|
114
|
+
const structure = inspectArtifactContract(content, getArtifactSchema(spec.family));
|
|
115
|
+
const expected = expectedQualificationRelations(taskContent, spec.family);
|
|
116
|
+
if (!expected.ok)
|
|
117
|
+
return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
|
|
118
|
+
const qualification = validateQualificationAudit(taskContent, content, {
|
|
119
|
+
family: spec.family,
|
|
120
|
+
artifact: request.artifact,
|
|
121
|
+
expectedUpstreamRelations: expected.relations
|
|
122
|
+
});
|
|
123
|
+
const qualificationError = qualification.ok ? null : qualification;
|
|
124
|
+
if (!detail.ok || !structure.ok || qualificationError) {
|
|
125
|
+
if (!recovery && !request.dryRun) {
|
|
126
|
+
try {
|
|
127
|
+
recovery = beginArtifactRecovery({
|
|
128
|
+
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
129
|
+
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
130
|
+
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const diagnostics = [
|
|
137
|
+
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
138
|
+
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
|
|
139
|
+
...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
|
|
140
|
+
].join('; ');
|
|
141
|
+
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
|
|
142
|
+
artifactSha256, semanticDigest,
|
|
143
|
+
...(recovery ? { recovery: recoveryInfo(recovery) } : {})
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (!recovery && existingIntent?.state === 'passed') {
|
|
147
|
+
return {
|
|
148
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest }),
|
|
149
|
+
status: 'passed', changed: false, error: null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
|
|
153
|
+
return {
|
|
154
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
|
|
155
|
+
status: 'passed', changed: false, error: null
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
recovery ??= beginArtifactRecovery({
|
|
160
|
+
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
161
|
+
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
162
|
+
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
163
|
+
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
164
|
+
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
165
|
+
return {
|
|
166
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
|
|
167
|
+
status: 'passed', changed: false, error: null
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function preflightReviewSummary(request, options = {}) {
|
|
175
|
+
if (request.dryRun || options.lockAlreadyHeld)
|
|
176
|
+
return preflightReviewSummaryUnlocked(request, options);
|
|
177
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
178
|
+
if (!resolved.ok)
|
|
179
|
+
return preflightReviewSummaryUnlocked(request, options);
|
|
180
|
+
try {
|
|
181
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.preflight', () => preflightReviewSummaryUnlocked(request, { ...options, lockAlreadyHeld: true }));
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (!(error instanceof TaskExecutionLockError))
|
|
185
|
+
throw error;
|
|
186
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
|
|
53
187
|
}
|
|
54
188
|
}
|
|
189
|
+
function recoveryInfo(recovery) {
|
|
190
|
+
return {
|
|
191
|
+
recoveryId: recovery.recoveryId,
|
|
192
|
+
candidatePath: recovery.stagingPath,
|
|
193
|
+
baselineSha256: recovery.baselineSha256,
|
|
194
|
+
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
195
|
+
};
|
|
196
|
+
}
|
|
55
197
|
/** Prepare domain validation, summary bytes and provenance before any publication. */
|
|
56
198
|
function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
57
199
|
let taskId = null;
|
|
@@ -85,43 +227,41 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
85
227
|
catch (error) {
|
|
86
228
|
return reject('REVIEW_ARTIFACT_NOT_REGULAR', String(error));
|
|
87
229
|
}
|
|
230
|
+
let recovery;
|
|
231
|
+
if (request.recoveryId) {
|
|
232
|
+
try {
|
|
233
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
|
|
234
|
+
if (!intent || intent.recoveryOperationId !== request.recoveryId)
|
|
235
|
+
return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
|
|
236
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state))
|
|
237
|
+
return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
|
|
238
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
239
|
+
artifactContent = fs.existsSync(recovery.stagingPath)
|
|
240
|
+
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
241
|
+
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return reject('REVIEW_PROVENANCE_INVALID', String(error));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
88
247
|
const artifactSha256 = sha256Content(artifactContent);
|
|
89
248
|
const semanticDigest = canonicalSemanticDigest(artifactContent);
|
|
90
249
|
const digests = { artifactSha256, semanticDigest };
|
|
91
250
|
let repairIntent;
|
|
92
251
|
try {
|
|
93
|
-
repairIntent =
|
|
252
|
+
repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId, spec.family, request.artifact);
|
|
94
253
|
}
|
|
95
254
|
catch (error) {
|
|
96
255
|
return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
|
|
97
256
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const repairable = structure.repair !== null && structure.diagnostics.length === 1 && structure.diagnostics[0]?.repairable === true;
|
|
102
|
-
if (repairable && repairIntent && (repairIntent.state !== 'awaiting-repair'
|
|
103
|
-
|| repairIntent.artifactSha256 !== artifactSha256 || repairIntent.semanticDigest !== structure.semanticDigest)) {
|
|
104
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'a different review repair baseline is already recorded for this artifact', digests);
|
|
257
|
+
if (repairIntent?.state === 'consumed') {
|
|
258
|
+
if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
|
|
259
|
+
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
|
|
105
260
|
}
|
|
106
|
-
return {
|
|
107
|
-
...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
108
|
-
artifactSha256, semanticDigest: structure.semanticDigest, repairable, operation: structure.repair
|
|
109
|
-
}),
|
|
110
|
-
...(repairable && !repairIntent ? { provenance: {
|
|
111
|
-
version: 2, taskId: taskId, family: spec.family, artifact: request.artifact,
|
|
112
|
-
state: 'awaiting-repair', baselineSemanticDigest: structure.semanticDigest,
|
|
113
|
-
artifactSha256, semanticDigest: structure.semanticDigest,
|
|
114
|
-
recoveryOperationId: null, phase: null, authorityDigest: null,
|
|
115
|
-
requestId: `review-finalize:${taskId}`, createdAt: Date.now(), updatedAt: Date.now()
|
|
116
|
-
}, expectedIntent: null } : {})
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (repairIntent?.state === 'awaiting-repair' && repairIntent.baselineSemanticDigest !== semanticDigest) {
|
|
120
|
-
return reject('REVIEW_PROVENANCE_INVALID', 'the repaired review artifact semantic digest does not match the recorded repair baseline', digests);
|
|
121
261
|
}
|
|
122
|
-
if (
|
|
123
|
-
&& (repairIntent.
|
|
124
|
-
|
|
262
|
+
if (repairIntent?.state === 'passed'
|
|
263
|
+
&& (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
|
|
264
|
+
repairIntent = undefined;
|
|
125
265
|
}
|
|
126
266
|
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
|
|
127
267
|
return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
|
|
@@ -149,10 +289,68 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
149
289
|
const detail = inspectDecisionDetailDuplicates(artifactContent);
|
|
150
290
|
if (!detail.ok)
|
|
151
291
|
return reject('REVIEW_DECISION_DETAIL_INVALID', `${detail.code}: ${detail.message}`);
|
|
292
|
+
const schema = getArtifactSchema(spec.family);
|
|
293
|
+
const structure = inspectArtifactContract(artifactContent, schema);
|
|
294
|
+
if (!structure.ok) {
|
|
295
|
+
if (!recovery && !request.dryRun && options.startRecovery) {
|
|
296
|
+
try {
|
|
297
|
+
recovery = beginArtifactRecovery({
|
|
298
|
+
taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
299
|
+
requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
|
|
300
|
+
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
return reject('REVIEW_PROVENANCE_INVALID', String(error), digests);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
...reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', structure.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; '), {
|
|
308
|
+
artifactSha256, semanticDigest: structure.semanticDigest,
|
|
309
|
+
...(recovery ? { recovery: {
|
|
310
|
+
recoveryId: recovery.recoveryId,
|
|
311
|
+
candidatePath: recovery.stagingPath,
|
|
312
|
+
baselineSha256: recovery.baselineSha256,
|
|
313
|
+
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
314
|
+
} } : {})
|
|
315
|
+
}),
|
|
316
|
+
...(recovery ? { recovery } : {})
|
|
317
|
+
};
|
|
318
|
+
}
|
|
152
319
|
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts);
|
|
153
320
|
if (!transformed.ok)
|
|
154
321
|
return reject(transformed.code, transformed.message);
|
|
155
322
|
const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
|
|
323
|
+
if (!recovery && repairIntent?.state === 'commit-started') {
|
|
324
|
+
if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
|
|
325
|
+
return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
|
|
326
|
+
}
|
|
327
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
|
|
328
|
+
}
|
|
329
|
+
if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
|
|
330
|
+
if (transformed.changed)
|
|
331
|
+
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
|
|
332
|
+
return {
|
|
333
|
+
content: transformed.content,
|
|
334
|
+
result: {
|
|
335
|
+
...failed(request, 'REVIEW_ARTIFACT_CONFLICT', '', taskId, stageStatus, finalDigests),
|
|
336
|
+
status: 'no-op',
|
|
337
|
+
changed: false,
|
|
338
|
+
error: null
|
|
339
|
+
},
|
|
340
|
+
lockAlreadyHeld: options.lockAlreadyHeld
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
if (!recovery && !request.dryRun && options.startRecovery) {
|
|
344
|
+
try {
|
|
345
|
+
recovery = beginArtifactRecovery({
|
|
346
|
+
taskId: taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
347
|
+
requestId: `review-finalize:${taskId}:${stage}:${parsed.round}`
|
|
348
|
+
}, Buffer.from(artifactContent, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
return reject('REVIEW_PROVENANCE_INVALID', String(error), finalDigests);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
156
354
|
return {
|
|
157
355
|
content: transformed.content,
|
|
158
356
|
result: {
|
|
@@ -162,22 +360,64 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
162
360
|
operations: transformed.changed ? [{ kind: 'artifact', artifact: request.artifact, operation: 'update' }] : [],
|
|
163
361
|
error: null
|
|
164
362
|
},
|
|
165
|
-
...(
|
|
166
|
-
|
|
167
|
-
expectedIntent: repairIntent
|
|
168
|
-
} : {})
|
|
363
|
+
...(recovery ? { recovery } : {}),
|
|
364
|
+
lockAlreadyHeld: options.lockAlreadyHeld
|
|
169
365
|
};
|
|
170
366
|
}
|
|
171
|
-
function commitReviewSummaryProvenance(prepared, repoRoot) {
|
|
367
|
+
function commitReviewSummaryProvenance(prepared, repoRoot, options = {}) {
|
|
368
|
+
if (prepared.result.status === 'failed' || !prepared.recovery)
|
|
369
|
+
return prepared.result;
|
|
172
370
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
371
|
+
const intent = readArtifactRecoveryIntent(prepared.recovery.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
|
|
372
|
+
if (!intent)
|
|
373
|
+
throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
|
|
374
|
+
let committed;
|
|
375
|
+
if (intent.state === 'passed' || intent.state === 'consumed') {
|
|
376
|
+
return {
|
|
377
|
+
...prepared.result,
|
|
378
|
+
status: 'no-op',
|
|
379
|
+
changed: false,
|
|
380
|
+
artifactSha256: intent.finalArtifactSha256,
|
|
381
|
+
semanticDigest: intent.finalSemanticDigest,
|
|
382
|
+
error: null
|
|
383
|
+
};
|
|
175
384
|
}
|
|
176
|
-
|
|
385
|
+
else if (intent.state === 'commit-started') {
|
|
386
|
+
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
387
|
+
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
388
|
+
afterPublish: options.afterPublish
|
|
389
|
+
});
|
|
390
|
+
committed = reconciled.intent;
|
|
391
|
+
}
|
|
392
|
+
else if (intent.state === 'awaiting-preflight-recovery') {
|
|
393
|
+
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
394
|
+
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
395
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
396
|
+
}
|
|
397
|
+
else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
398
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
399
|
+
}
|
|
400
|
+
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
401
|
+
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
402
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
403
|
+
}
|
|
404
|
+
else if (intent.state === 'full-finalizer-ready') {
|
|
405
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
406
|
+
}
|
|
407
|
+
if (!committed || committed.state !== 'passed')
|
|
408
|
+
throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
409
|
+
return {
|
|
410
|
+
...prepared.result,
|
|
411
|
+
artifactSha256: committed.finalArtifactSha256,
|
|
412
|
+
semanticDigest: committed.finalSemanticDigest,
|
|
413
|
+
error: null
|
|
414
|
+
};
|
|
177
415
|
}
|
|
178
416
|
catch (error) {
|
|
417
|
+
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:'))
|
|
418
|
+
throw error;
|
|
179
419
|
return { ...prepared.result, status: 'failed', error: {
|
|
180
|
-
code: '
|
|
420
|
+
code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
|
|
181
421
|
} };
|
|
182
422
|
}
|
|
183
423
|
}
|
|
@@ -203,37 +443,17 @@ function finalizeReviewSummaryUnlocked(request, options = {}) {
|
|
|
203
443
|
const prepared = prepareReviewSummaryCandidate(request, content, options);
|
|
204
444
|
if (request.dryRun)
|
|
205
445
|
return prepared.result;
|
|
206
|
-
if (prepared.result.status !== 'applied')
|
|
207
|
-
return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
|
|
208
|
-
const tempPath = path.join(resolved.taskDir, `.${request.artifact}.tmp-${process.pid}-${(options.randomSuffix ?? randomUUID)()}`);
|
|
209
|
-
try {
|
|
210
|
-
fileSystem.writeFileSync(tempPath, prepared.content, fileSystem.statModeSync(validated.artifact.path));
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
cleanupTemp(fileSystem, tempPath);
|
|
214
|
-
return failed(request, 'REVIEW_TEMP_WRITE_FAILED', String(error), resolved.taskId, prepared.result.stageStatus);
|
|
215
|
-
}
|
|
216
|
-
try {
|
|
217
|
-
if (fileSystem.readFileSync(validated.artifact.path) !== content) {
|
|
218
|
-
cleanupTemp(fileSystem, tempPath);
|
|
219
|
-
return failed(request, 'REVIEW_ARTIFACT_CONFLICT', `${request.artifact} changed during finalization`, resolved.taskId, prepared.result.stageStatus);
|
|
220
|
-
}
|
|
221
|
-
fileSystem.renameSync(tempPath, validated.artifact.path);
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
cleanupTemp(fileSystem, tempPath);
|
|
225
|
-
return failed(request, 'REVIEW_RENAME_FAILED', String(error), resolved.taskId, prepared.result.stageStatus);
|
|
226
|
-
}
|
|
227
446
|
return commitReviewSummaryProvenance(prepared, resolved.repoRoot);
|
|
228
447
|
}
|
|
229
448
|
function finalizeReviewSummary(request, options = {}) {
|
|
449
|
+
const effectiveOptions = { ...options, startRecovery: !request.dryRun };
|
|
230
450
|
if (request.dryRun || options.lockAlreadyHeld)
|
|
231
|
-
return finalizeReviewSummaryUnlocked(request,
|
|
451
|
+
return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
232
452
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
233
453
|
if (!resolved.ok)
|
|
234
|
-
return finalizeReviewSummaryUnlocked(request,
|
|
454
|
+
return finalizeReviewSummaryUnlocked(request, effectiveOptions);
|
|
235
455
|
try {
|
|
236
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', () => finalizeReviewSummaryUnlocked(request,
|
|
456
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'task-review.finalize-summary', () => finalizeReviewSummaryUnlocked(request, { ...effectiveOptions, lockAlreadyHeld: true }));
|
|
237
457
|
}
|
|
238
458
|
catch (error) {
|
|
239
459
|
if (!(error instanceof TaskExecutionLockError))
|
|
@@ -241,5 +461,5 @@ function finalizeReviewSummary(request, options = {}) {
|
|
|
241
461
|
return failed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
|
|
242
462
|
}
|
|
243
463
|
}
|
|
244
|
-
export { finalizeReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
|
|
464
|
+
export { finalizeReviewSummary, preflightReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
|
|
245
465
|
//# sourceMappingURL=review-finalization.js.map
|