@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
package/lib/task/lifecycle.ts
CHANGED
|
@@ -15,9 +15,11 @@ import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
|
15
15
|
import type { TaskFileSystem, TaskMutation, TaskOperationSummary, TaskWriteMetadata } from './write.ts';
|
|
16
16
|
import { validateCurrentTaskContract } from './current-contract.ts';
|
|
17
17
|
import { isValidAgentInfraVersion } from '../version.ts';
|
|
18
|
+
import { taskIssueIdentity } from '../platform/task-identities.ts';
|
|
19
|
+
import { resourceIdentityEquals } from '../platform/resource-identity.ts';
|
|
18
20
|
|
|
19
21
|
const lifecycleIntentCatalog = [
|
|
20
|
-
'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'
|
|
22
|
+
'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore', 'recover-started'
|
|
21
23
|
] as const;
|
|
22
24
|
const lifecycleFailureCatalog = [
|
|
23
25
|
'LIFECYCLE_DIRECTORY_RENAME_FAILED',
|
|
@@ -55,7 +57,8 @@ type TaskLifecycleRequest =
|
|
|
55
57
|
| { taskRef: string; intent: 'complete'; agent: string; dryRun?: boolean }
|
|
56
58
|
| { taskRef: string; intent: 'close-codescan'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
57
59
|
| { taskRef: string; intent: 'close-dependabot'; agent: string; alertNumber: number; reason: string; dryRun?: boolean }
|
|
58
|
-
| { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
|
|
60
|
+
| { taskRef: string; intent: 'restore'; agent: string; stagingDir: string; issueNumber: number; dryRun?: boolean }
|
|
61
|
+
| { taskRef: string; intent: 'recover-started'; agent: string; stage: 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code'; round: number; artifact: string; reason: string; dryRun?: boolean };
|
|
59
62
|
|
|
60
63
|
type HotState = 'active' | 'blocked' | 'completed';
|
|
61
64
|
type SourceState = HotState | 'staging';
|
|
@@ -206,6 +209,7 @@ function actionAndNote(request: TaskLifecycleRequest, restoredFiles = 0): { acti
|
|
|
206
209
|
if (request.intent === 'complete') return { action: 'Complete Task', note: 'Task moved to completed/' };
|
|
207
210
|
if (request.intent === 'close-codescan') return { action: 'Close Codescan', note: `Code Scanning alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
208
211
|
if (request.intent === 'close-dependabot') return { action: 'Close Dependabot', note: `Dependabot alert #${request.alertNumber} dismissed: ${request.reason}` };
|
|
212
|
+
if (request.intent === 'recover-started') return { action: 'Recover Started', note: request.reason };
|
|
209
213
|
return { action: 'Restore Task', note: `Restored ${restoredFiles} files from Issue #${request.issueNumber}` };
|
|
210
214
|
}
|
|
211
215
|
|
|
@@ -249,8 +253,10 @@ function validateRestoreStaging(request: Extract<TaskLifecycleRequest, { intent:
|
|
|
249
253
|
if (!fs.existsSync(taskFile)) return { code: 'LIFECYCLE_STAGING_INVALID', message: 'restore staging has no task.md' };
|
|
250
254
|
try {
|
|
251
255
|
const frontmatter = parseTypedTaskFrontmatter(fs.readFileSync(taskFile, 'utf8'));
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
const issueIdentity = taskIssueIdentity(frontmatter);
|
|
257
|
+
const requestedIssueIdentity = { kind: 'number' as const, value: request.issueNumber };
|
|
258
|
+
if (frontmatter.id !== taskId || !issueIdentity || !resourceIdentityEquals(issueIdentity, requestedIssueIdentity) || frontmatter.current_step === 'completed') {
|
|
259
|
+
return { code: 'LIFECYCLE_STAGING_IDENTITY_INVALID', message: 'restore task identity, platform issue identity, or current_step is invalid' };
|
|
254
260
|
}
|
|
255
261
|
} catch (error) {
|
|
256
262
|
return { code: 'LIFECYCLE_STAGING_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
@@ -410,6 +416,9 @@ function removeCopiedSource(source: string, target: string): void {
|
|
|
410
416
|
}
|
|
411
417
|
|
|
412
418
|
function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLifecycleOptions = {}): TaskLifecycleResult {
|
|
419
|
+
if (requestInput.intent === 'recover-started') {
|
|
420
|
+
return failed(requestInput, { code: 'LIFECYCLE_RECOVERY_REQUIRES_AUTHORITY', message: 'recover-started must be dispatched through task control authority' });
|
|
421
|
+
}
|
|
413
422
|
const normalized = normalizedRequest(requestInput);
|
|
414
423
|
if ('code' in normalized) return failed(requestInput, normalized);
|
|
415
424
|
const request = normalized;
|
|
@@ -5,6 +5,7 @@ import { scanVisibleMarkdown } from './markdown.ts';
|
|
|
5
5
|
import { parseArtifactName } from './artifact-name.ts';
|
|
6
6
|
import {
|
|
7
7
|
hasOpenArtifactRound,
|
|
8
|
+
parseCodePlanInputReference,
|
|
8
9
|
validateArtifactPublication,
|
|
9
10
|
validateCompletedArtifact
|
|
10
11
|
} from './artifact-lifecycle.ts';
|
|
@@ -12,8 +13,20 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
12
13
|
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
13
14
|
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
|
|
14
15
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
15
|
-
import {
|
|
16
|
-
import type {
|
|
16
|
+
import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
17
|
+
import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
18
|
+
import {
|
|
19
|
+
beginArtifactRecovery,
|
|
20
|
+
commitArtifactRecovery,
|
|
21
|
+
consumeArtifactRecovery,
|
|
22
|
+
prepareArtifactRecoveryFinal,
|
|
23
|
+
prepareArtifactRecoveryCommit,
|
|
24
|
+
recordArtifactRecoveryPassed,
|
|
25
|
+
reconcileArtifactRecovery,
|
|
26
|
+
recoveryContextFromIntent,
|
|
27
|
+
stageArtifactCandidate
|
|
28
|
+
} from './artifact-recovery.ts';
|
|
29
|
+
import type { ArtifactRecoveryContext } from './artifact-recovery.ts';
|
|
17
30
|
import {
|
|
18
31
|
consumeLifecycleRecoveryAttestation,
|
|
19
32
|
lifecycleRecoveryAttestationDigest,
|
|
@@ -38,18 +51,14 @@ type LocalArtifactDiagnosticCode =
|
|
|
38
51
|
| 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
39
52
|
| 'LOCAL_STRUCTURAL_INVALID'
|
|
40
53
|
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
41
|
-
| '
|
|
42
|
-
| '
|
|
54
|
+
| 'LOCAL_RECOVERY_PROVENANCE_CONFLICT'
|
|
55
|
+
| 'LOCAL_RECOVERY_BASELINE_MISMATCH'
|
|
43
56
|
| 'LOCAL_QUALIFICATION_AUDIT_INVALID';
|
|
44
57
|
|
|
45
58
|
type LocalArtifactDiagnostic = {
|
|
46
59
|
code: LocalArtifactDiagnosticCode;
|
|
47
60
|
message: string;
|
|
48
|
-
repairable: boolean;
|
|
49
61
|
line: number | null;
|
|
50
|
-
from?: string;
|
|
51
|
-
to?: string;
|
|
52
|
-
operation?: 'replace-line';
|
|
53
62
|
};
|
|
54
63
|
|
|
55
64
|
type LocalArtifactValidationOptions = {
|
|
@@ -63,7 +72,6 @@ type LocalArtifactValidationResult = {
|
|
|
63
72
|
ok: boolean;
|
|
64
73
|
family: LocalArtifactFamily;
|
|
65
74
|
semanticDigest: string;
|
|
66
|
-
repairable: boolean;
|
|
67
75
|
diagnostics: readonly LocalArtifactDiagnostic[];
|
|
68
76
|
};
|
|
69
77
|
|
|
@@ -73,23 +81,32 @@ type LocalArtifactFinalizationRequest = {
|
|
|
73
81
|
artifact: string;
|
|
74
82
|
repoRoot?: string;
|
|
75
83
|
requiredSections?: readonly string[];
|
|
84
|
+
recoveryId?: string;
|
|
85
|
+
lockAlreadyHeld?: boolean;
|
|
76
86
|
};
|
|
77
87
|
|
|
88
|
+
type LocalArtifactRecoveryInfo = Readonly<{
|
|
89
|
+
recoveryId: string;
|
|
90
|
+
candidatePath: string;
|
|
91
|
+
baselineSha256: string;
|
|
92
|
+
baselineSemanticDigest: string;
|
|
93
|
+
}>;
|
|
94
|
+
|
|
78
95
|
type LocalArtifactFinalizationResult = {
|
|
79
96
|
status: 'passed' | 'failed';
|
|
80
|
-
changed:
|
|
97
|
+
changed: boolean;
|
|
81
98
|
taskId: string | null;
|
|
82
99
|
taskDir: string | null;
|
|
83
100
|
family: LocalArtifactFamily;
|
|
84
101
|
artifact: string;
|
|
85
102
|
artifactSha256: string | null;
|
|
86
103
|
semanticDigest: string | null;
|
|
87
|
-
repairable: boolean;
|
|
88
104
|
diagnostics: readonly LocalArtifactDiagnostic[];
|
|
105
|
+
recovery?: LocalArtifactRecoveryInfo;
|
|
89
106
|
error: { code: string; message: string } | null;
|
|
90
107
|
};
|
|
91
108
|
|
|
92
|
-
type LocalArtifactFinalizationIntent =
|
|
109
|
+
type LocalArtifactFinalizationIntent = ArtifactRecoveryIntent;
|
|
93
110
|
|
|
94
111
|
function authorityDigest(authority: LifecycleRecoveryAttestationV1): string {
|
|
95
112
|
return lifecycleRecoveryAttestationDigest(authority);
|
|
@@ -113,18 +130,27 @@ function validFinalizerAuthority(
|
|
|
113
130
|
&& authority.operationId.length > 0;
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
function recoveryInfo(context: ArtifactRecoveryContext): LocalArtifactRecoveryInfo {
|
|
134
|
+
return {
|
|
135
|
+
recoveryId: context.recoveryId,
|
|
136
|
+
candidatePath: context.stagingPath,
|
|
137
|
+
baselineSha256: context.baselineSha256,
|
|
138
|
+
baselineSemanticDigest: context.baselineSemanticDigest
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
116
142
|
function consumeLocalArtifactFinalizationIntent(
|
|
117
143
|
repoRoot: string,
|
|
118
|
-
intent: LocalArtifactFinalizationIntent
|
|
144
|
+
intent: LocalArtifactFinalizationIntent,
|
|
145
|
+
options: Readonly<{ lockAlreadyHeld?: boolean }> = {}
|
|
119
146
|
): LocalArtifactFinalizationIntent {
|
|
120
147
|
if (intent.state === 'consumed') return intent;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return consumed;
|
|
148
|
+
const resolved = resolveTaskRef(intent.taskId, { repoRoot });
|
|
149
|
+
if (!resolved.ok) throw new Error(resolved.message);
|
|
150
|
+
return consumeArtifactRecovery(recoveryContextFromIntent(repoRoot, resolved.taskDir, intent), options);
|
|
125
151
|
}
|
|
126
152
|
|
|
127
|
-
function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode
|
|
153
|
+
function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode {
|
|
128
154
|
const map: Record<string, LocalArtifactDiagnosticCode> = {
|
|
129
155
|
ARTIFACT_EMPTY: 'LOCAL_ARTIFACT_EMPTY',
|
|
130
156
|
ARTIFACT_MISSING_SECTION: 'LOCAL_ARTIFACT_MISSING_SECTION',
|
|
@@ -134,20 +160,8 @@ function localDiagnosticCode(code: string): LocalArtifactDiagnosticCode | null {
|
|
|
134
160
|
return map[code] ?? 'LOCAL_STRUCTURAL_INVALID';
|
|
135
161
|
}
|
|
136
162
|
|
|
137
|
-
function localDiagnostic(
|
|
138
|
-
|
|
139
|
-
): LocalArtifactDiagnostic | null {
|
|
140
|
-
const code = localDiagnosticCode(item.code);
|
|
141
|
-
if (!code) return null;
|
|
142
|
-
return {
|
|
143
|
-
code,
|
|
144
|
-
message: item.message,
|
|
145
|
-
repairable: item.repairable,
|
|
146
|
-
line: item.line,
|
|
147
|
-
...(item.operation?.kind === 'replace-line'
|
|
148
|
-
? { from: item.operation.from, to: item.operation.to, operation: 'replace-line' as const }
|
|
149
|
-
: {})
|
|
150
|
-
};
|
|
163
|
+
function localDiagnostic(item: { code: string; message: string; line: number | null }): LocalArtifactDiagnostic {
|
|
164
|
+
return { code: localDiagnosticCode(item.code), message: item.message, line: item.line };
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
function isStatusPattern(pattern: string): boolean {
|
|
@@ -159,14 +173,10 @@ function validateLocalArtifact(
|
|
|
159
173
|
options: LocalArtifactValidationOptions
|
|
160
174
|
): LocalArtifactValidationResult {
|
|
161
175
|
const schema = getArtifactSchema(options.family)!;
|
|
162
|
-
const patterns = schema.requiredPatterns;
|
|
163
176
|
const diagnostics: LocalArtifactDiagnostic[] = [];
|
|
164
177
|
const scanned = scanVisibleMarkdown(content);
|
|
165
178
|
const structure = inspectArtifactStructure(content, schema);
|
|
166
|
-
for (const item of structure.diagnostics)
|
|
167
|
-
const mapped = localDiagnostic(item);
|
|
168
|
-
if (mapped) diagnostics.push(mapped);
|
|
169
|
-
}
|
|
179
|
+
for (const item of structure.diagnostics) diagnostics.push(localDiagnostic(item));
|
|
170
180
|
|
|
171
181
|
const statusSection = schema.sections.find((section) => section.id === 'state-check');
|
|
172
182
|
if (statusSection) {
|
|
@@ -178,9 +188,9 @@ function validateLocalArtifact(
|
|
|
178
188
|
if (statusHeading) {
|
|
179
189
|
const next = scanned.headings.find((candidate) => candidate.start > statusHeading.start && candidate.level <= 2);
|
|
180
190
|
const body = content.slice(statusHeading.end, next?.start ?? content.length);
|
|
181
|
-
for (const pattern of
|
|
191
|
+
for (const pattern of schema.requiredPatterns.filter(isStatusPattern)) {
|
|
182
192
|
if (!new RegExp(pattern, 'm').test(body)) {
|
|
183
|
-
diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`,
|
|
193
|
+
diagnostics.push({ code: 'LOCAL_STATUS_COMMAND_MISSING', message: `status section '${statusHeading.text}' is missing required command output`, line: content.slice(0, statusHeading.start).split('\n').length });
|
|
184
194
|
}
|
|
185
195
|
}
|
|
186
196
|
}
|
|
@@ -188,40 +198,28 @@ function validateLocalArtifact(
|
|
|
188
198
|
|
|
189
199
|
const patternInspection = inspectArtifactPatterns(content, {
|
|
190
200
|
...schema,
|
|
191
|
-
requiredPatterns:
|
|
201
|
+
requiredPatterns: schema.requiredPatterns.filter((item) => !isStatusPattern(item))
|
|
192
202
|
});
|
|
193
|
-
for (const item of patternInspection.diagnostics) {
|
|
194
|
-
diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, repairable: false, line: null });
|
|
195
|
-
}
|
|
203
|
+
for (const item of patternInspection.diagnostics) diagnostics.push({ code: 'LOCAL_REQUIRED_PATTERN_MISSING', message: item.message, line: null });
|
|
196
204
|
|
|
197
205
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
198
|
-
if (!decisionDetails.ok) {
|
|
199
|
-
diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, repairable: false, line: null });
|
|
200
|
-
}
|
|
206
|
+
if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
201
207
|
|
|
202
208
|
if (options.taskContent !== undefined) {
|
|
203
209
|
const expected = expectedQualificationRelations(options.taskContent, options.family);
|
|
204
|
-
if (!expected.ok) {
|
|
205
|
-
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, repairable: false, line: null });
|
|
206
|
-
}
|
|
210
|
+
if (!expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
207
211
|
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
208
|
-
family: options.family
|
|
212
|
+
family: options.family,
|
|
209
213
|
artifact: options.artifact,
|
|
210
214
|
expectedUpstreamRelations: expected.ok ? expected.relations : undefined
|
|
211
215
|
});
|
|
212
|
-
if (!qualification.ok) {
|
|
213
|
-
diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, repairable: false, line: null });
|
|
214
|
-
}
|
|
216
|
+
if (!qualification.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
const repairable = structure.repair !== null && diagnostics.length === 1 && diagnostics[0]?.repairable === true;
|
|
218
|
-
const semanticDigestValue = canonicalSemanticDigest(content, structure.repair);
|
|
219
|
-
|
|
220
219
|
return {
|
|
221
220
|
ok: diagnostics.length === 0,
|
|
222
221
|
family: options.family,
|
|
223
|
-
semanticDigest:
|
|
224
|
-
repairable,
|
|
222
|
+
semanticDigest: canonicalSemanticDigest(content),
|
|
225
223
|
diagnostics
|
|
226
224
|
};
|
|
227
225
|
}
|
|
@@ -236,7 +234,7 @@ function failedFinalization(
|
|
|
236
234
|
taskId: null, taskDir: null,
|
|
237
235
|
family: request.family, artifact: request.artifact,
|
|
238
236
|
artifactSha256: null, semanticDigest: null,
|
|
239
|
-
|
|
237
|
+
diagnostics: [], error, ...extra
|
|
240
238
|
};
|
|
241
239
|
}
|
|
242
240
|
|
|
@@ -244,114 +242,222 @@ type LocalArtifactPreparation = Readonly<{
|
|
|
244
242
|
result: LocalArtifactFinalizationResult;
|
|
245
243
|
content: string;
|
|
246
244
|
repoRoot?: string;
|
|
247
|
-
|
|
248
|
-
|
|
245
|
+
lockAlreadyHeld?: boolean;
|
|
246
|
+
recovery?: ArtifactRecoveryContext;
|
|
247
|
+
authority?: LifecycleRecoveryAttestationV1;
|
|
249
248
|
}>;
|
|
250
249
|
|
|
251
|
-
|
|
250
|
+
function tupleFor(
|
|
251
|
+
request: LocalArtifactFinalizationRequest,
|
|
252
|
+
taskId: string,
|
|
253
|
+
authority?: LifecycleRecoveryAttestationV1
|
|
254
|
+
) {
|
|
255
|
+
const round = parseArtifactName(request.artifact)?.round ?? 0;
|
|
256
|
+
return {
|
|
257
|
+
taskId,
|
|
258
|
+
family: request.family,
|
|
259
|
+
artifact: request.artifact,
|
|
260
|
+
round,
|
|
261
|
+
requestId: authority?.lifecycleRequestId ?? `local-finalize:${taskId}:${request.family}:${round}`,
|
|
262
|
+
phase: authority?.phase ?? 'artifact.finalize-local' as const,
|
|
263
|
+
authorityDigest: authority ? authorityDigest(authority) : null
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
252
267
|
function prepareLocalArtifact(
|
|
253
268
|
request: LocalArtifactFinalizationRequest,
|
|
254
269
|
candidate?: string,
|
|
255
|
-
authority?: LifecycleRecoveryAttestationV1
|
|
270
|
+
authority?: LifecycleRecoveryAttestationV1,
|
|
271
|
+
preflightOnly = false
|
|
256
272
|
): LocalArtifactPreparation {
|
|
257
|
-
const failed = (code: string, message: string): LocalArtifactPreparation => ({
|
|
258
|
-
result: failedFinalization(request, { code, message }), content: candidate ?? ''
|
|
273
|
+
const failed = (code: string, message: string, extra: Partial<LocalArtifactFinalizationResult> = {}): LocalArtifactPreparation => ({
|
|
274
|
+
result: failedFinalization(request, { code, message }, extra), content: candidate ?? ''
|
|
259
275
|
});
|
|
260
276
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: request.repoRoot });
|
|
261
277
|
if (!resolved.ok) return failed(resolved.code, resolved.message);
|
|
262
278
|
const parsed = parseArtifactName(request.artifact);
|
|
263
279
|
if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
|
|
264
|
-
|
|
280
|
+
if (authority && !validFinalizerAuthority(authority, request, resolved.taskId)) {
|
|
281
|
+
return failed('LOCAL_EXECUTION_AUTHORITY_INVALID', 'finalizer authority does not match the artifact tuple');
|
|
282
|
+
}
|
|
283
|
+
|
|
265
284
|
let taskContent: string;
|
|
285
|
+
let content: string;
|
|
286
|
+
let recovery: ArtifactRecoveryContext | undefined;
|
|
287
|
+
let recoveryState: ArtifactRecoveryIntent['state'] | undefined;
|
|
266
288
|
try {
|
|
267
289
|
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
268
|
-
if (
|
|
269
|
-
const
|
|
270
|
-
if (
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
content =
|
|
290
|
+
if (request.recoveryId) {
|
|
291
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
292
|
+
if (!intent || intent.recoveryOperationId !== request.recoveryId) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'recovery id does not match the artifact journal');
|
|
293
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state)) return failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `recovery journal is in '${intent.state}' state`);
|
|
294
|
+
recoveryState = intent.state;
|
|
295
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
296
|
+
content = fs.existsSync(recovery.stagingPath)
|
|
297
|
+
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
298
|
+
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
275
299
|
} else {
|
|
276
300
|
const validated = validateCompletedArtifact(resolved.taskDir, request.family, request.artifact, parsed.round);
|
|
277
301
|
if (!validated.ok) return failed(validated.error.code, validated.error.message);
|
|
278
|
-
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
302
|
+
content = candidate ?? fs.readFileSync(validated.artifact.path, 'utf8');
|
|
279
303
|
}
|
|
280
304
|
} catch (error) { return failed('ARTIFACT_NOT_READABLE', String(error)); }
|
|
305
|
+
|
|
306
|
+
if (resolved.state !== 'active' || !hasOpenArtifactRound(taskContent, request.family, parsed.round)) {
|
|
307
|
+
return failed('ARTIFACT_IDENTITY_INVALID', 'candidate must match one open started round in an active task');
|
|
308
|
+
}
|
|
281
309
|
const validation = validateLocalArtifact(content, {
|
|
282
310
|
family: request.family, requiredSections: request.requiredSections, taskContent, artifact: request.artifact
|
|
283
311
|
});
|
|
284
312
|
const artifactSha256 = sha256Content(content);
|
|
285
|
-
const { semanticDigest, repairable, diagnostics } = validation;
|
|
286
313
|
const result = failedFinalization(request, {
|
|
287
|
-
code: 'LOCAL_ARTIFACT_INVALID',
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
314
|
+
code: 'LOCAL_ARTIFACT_INVALID',
|
|
315
|
+
message: validation.diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
316
|
+
}, {
|
|
317
|
+
taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256,
|
|
318
|
+
semanticDigest: validation.semanticDigest, diagnostics: validation.diagnostics
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (request.recoveryId) {
|
|
322
|
+
if (!validation.ok) return { result, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
323
|
+
if (preflightOnly && recovery && recoveryState === 'awaiting-preflight-recovery') {
|
|
324
|
+
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
325
|
+
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
326
|
+
}
|
|
291
327
|
return {
|
|
292
|
-
...
|
|
293
|
-
|
|
294
|
-
code: 'LOCAL_EXECUTION_AUTHORITY_INVALID',
|
|
295
|
-
message: 'finalizer authority does not match the artifact tuple'
|
|
296
|
-
}, diagnostics: [] }
|
|
328
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(recovery!) },
|
|
329
|
+
content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
297
330
|
};
|
|
298
331
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
332
|
+
|
|
333
|
+
const existing = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, request.family, request.artifact);
|
|
334
|
+
if (existing?.state === 'passed' || existing?.state === 'consumed') {
|
|
335
|
+
if (existing.state === 'consumed' && (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest)) {
|
|
336
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match its completed recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
337
|
+
}
|
|
338
|
+
if (existing.finalArtifactSha256 === artifactSha256 && existing.finalSemanticDigest === validation.semanticDigest) {
|
|
339
|
+
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
340
|
+
return {
|
|
341
|
+
result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) },
|
|
342
|
+
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
343
|
+
};
|
|
344
|
+
}
|
|
308
345
|
}
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
return
|
|
346
|
+
|
|
347
|
+
if (existing && ['preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready'].includes(existing.state)) {
|
|
348
|
+
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
349
|
+
return {
|
|
350
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
|
|
351
|
+
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
352
|
+
};
|
|
313
353
|
}
|
|
314
|
-
if (
|
|
315
|
-
if (
|
|
316
|
-
return
|
|
354
|
+
if (existing?.state === 'commit-started') {
|
|
355
|
+
if (existing.finalArtifactSha256 !== artifactSha256 || existing.finalSemanticDigest !== validation.semanticDigest) {
|
|
356
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', 'formal artifact does not match the interrupted recovery journal'), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
317
357
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
358
|
+
const context = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, existing);
|
|
359
|
+
const reconciled = reconcileArtifactRecovery(context, {
|
|
360
|
+
lockAlreadyHeld: request.lockAlreadyHeld,
|
|
361
|
+
validateFinal: () => validation.semanticDigest === existing.finalSemanticDigest
|
|
362
|
+
});
|
|
363
|
+
if (reconciled.status !== 'passed') {
|
|
364
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', `interrupted recovery could not be reconciled: ${reconciled.status}`), content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
323
365
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
366
|
+
return {
|
|
367
|
+
result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) },
|
|
368
|
+
content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
if (validation.ok) {
|
|
374
|
+
if (preflightOnly) {
|
|
375
|
+
const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
|
|
376
|
+
repoRoot: resolved.repoRoot, taskDir: resolved.taskDir,
|
|
377
|
+
...(authority ? { recoveryId: authority.operationId } : {}), lockAlreadyHeld: request.lockAlreadyHeld
|
|
378
|
+
});
|
|
379
|
+
const staged = stageArtifactCandidate(context, Buffer.from(content, 'utf8'), { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
380
|
+
prepareArtifactRecoveryCommit(context, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: request.lockAlreadyHeld });
|
|
381
|
+
return { result: { ...result, status: 'passed', error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery: context, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
382
|
+
}
|
|
383
|
+
const context = recordArtifactRecoveryPassed(tupleFor(request, resolved.taskId, authority), {
|
|
384
|
+
repoRoot: resolved.repoRoot,
|
|
385
|
+
taskDir: resolved.taskDir,
|
|
386
|
+
expectedFinalSha256: artifactSha256,
|
|
387
|
+
expectedFinalSemanticDigest: validation.semanticDigest,
|
|
388
|
+
...(authority ? { recoveryId: authority.operationId } : {}),
|
|
389
|
+
lockAlreadyHeld: request.lockAlreadyHeld
|
|
390
|
+
});
|
|
391
|
+
recovery = context;
|
|
392
|
+
return { result: { ...result, status: 'passed', changed: false, error: null, recovery: recoveryInfo(context) }, content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
327
393
|
}
|
|
328
|
-
|
|
329
|
-
|
|
394
|
+
const context = beginArtifactRecovery(tupleFor(request, resolved.taskId, authority), Buffer.from(content, 'utf8'), {
|
|
395
|
+
repoRoot: resolved.repoRoot,
|
|
396
|
+
taskDir: resolved.taskDir,
|
|
397
|
+
...(authority ? { recoveryId: authority.operationId } : {}),
|
|
398
|
+
lockAlreadyHeld: request.lockAlreadyHeld
|
|
399
|
+
});
|
|
400
|
+
recovery = context;
|
|
401
|
+
return {
|
|
402
|
+
result: { ...result, recovery: recoveryInfo(context) },
|
|
403
|
+
content, repoRoot: resolved.repoRoot, recovery, authority, lockAlreadyHeld: request.lockAlreadyHeld
|
|
404
|
+
};
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return { ...failed('LOCAL_RECOVERY_PROVENANCE_CONFLICT', String(error)), content, repoRoot: resolved.repoRoot, authority, lockAlreadyHeld: request.lockAlreadyHeld };
|
|
330
407
|
}
|
|
331
|
-
const timestamp = Date.now();
|
|
332
|
-
return { ...prepared, provenance: {
|
|
333
|
-
version: 2, taskId: resolved.taskId, family: request.family, artifact: request.artifact,
|
|
334
|
-
state: repairable ? 'awaiting-repair' : 'passed',
|
|
335
|
-
baselineSemanticDigest: repairable ? semanticDigest : intent?.baselineSemanticDigest ?? null,
|
|
336
|
-
artifactSha256, semanticDigest,
|
|
337
|
-
recoveryOperationId: authority?.operationId ?? intent?.recoveryOperationId ?? null,
|
|
338
|
-
phase: authority?.phase ?? intent?.phase ?? null,
|
|
339
|
-
authorityDigest: authority ? authorityDigest(authority) : intent?.authorityDigest ?? null,
|
|
340
|
-
requestId: authority?.lifecycleRequestId ?? intent?.requestId ?? `local-finalize:${resolved.taskId}`,
|
|
341
|
-
createdAt: intent?.createdAt ?? timestamp,
|
|
342
|
-
updatedAt: timestamp
|
|
343
|
-
} };
|
|
344
408
|
}
|
|
345
409
|
|
|
346
|
-
function commitLocalArtifactProvenance(
|
|
410
|
+
function commitLocalArtifactProvenance(
|
|
411
|
+
prepared: LocalArtifactPreparation,
|
|
412
|
+
options: Readonly<{ afterPublish?: () => void }> = {}
|
|
413
|
+
): LocalArtifactFinalizationResult {
|
|
414
|
+
if (!prepared.recovery || !prepared.repoRoot || prepared.result.status === 'failed' && !prepared.result.recovery) return prepared.result;
|
|
415
|
+
if (prepared.result.status === 'failed') return prepared.result;
|
|
347
416
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
417
|
+
const intent = readArtifactRecoveryIntent(prepared.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
|
|
418
|
+
if (!intent) throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
|
|
419
|
+
let committed;
|
|
420
|
+
if (intent.state === 'passed' || intent.state === 'consumed') {
|
|
421
|
+
return {
|
|
422
|
+
...prepared.result,
|
|
423
|
+
changed: false,
|
|
424
|
+
artifactSha256: intent.finalArtifactSha256,
|
|
425
|
+
semanticDigest: intent.finalSemanticDigest,
|
|
426
|
+
error: null
|
|
427
|
+
};
|
|
428
|
+
} else if (intent.state === 'commit-started') {
|
|
429
|
+
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
430
|
+
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
431
|
+
afterPublish: options.afterPublish
|
|
432
|
+
});
|
|
433
|
+
committed = reconciled.intent;
|
|
434
|
+
} else if (intent.state === 'awaiting-preflight-recovery') {
|
|
435
|
+
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
436
|
+
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
437
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
438
|
+
}
|
|
439
|
+
if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
440
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
441
|
+
}
|
|
442
|
+
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
443
|
+
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
444
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
445
|
+
} else if (intent.state === 'full-finalizer-ready') {
|
|
446
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
350
447
|
}
|
|
351
|
-
|
|
448
|
+
if (!committed || committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
449
|
+
return {
|
|
450
|
+
...prepared.result,
|
|
451
|
+
status: 'passed',
|
|
452
|
+
changed: false,
|
|
453
|
+
artifactSha256: committed.finalArtifactSha256,
|
|
454
|
+
semanticDigest: committed.finalSemanticDigest,
|
|
455
|
+
error: null
|
|
456
|
+
};
|
|
352
457
|
} catch (error) {
|
|
458
|
+
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:')) throw error;
|
|
353
459
|
return { ...prepared.result, status: 'failed', error: {
|
|
354
|
-
code: '
|
|
460
|
+
code: 'LOCAL_RECOVERY_COMMIT_FAILED', message: String(error)
|
|
355
461
|
} };
|
|
356
462
|
}
|
|
357
463
|
}
|
|
@@ -361,7 +467,8 @@ function finalizeLocalArtifact(
|
|
|
361
467
|
authority?: LifecycleRecoveryAttestationV1,
|
|
362
468
|
options: Readonly<{ deferLifecycleRecoveryConsumption?: boolean }> = {}
|
|
363
469
|
): LocalArtifactFinalizationResult {
|
|
364
|
-
const
|
|
470
|
+
const prepared = prepareLocalArtifact(request, undefined, authority);
|
|
471
|
+
const result = commitLocalArtifactProvenance(prepared);
|
|
365
472
|
if (authority && !options.deferLifecycleRecoveryConsumption && result.status === 'passed') {
|
|
366
473
|
try { consumeLifecycleRecoveryAttestation(authority); }
|
|
367
474
|
catch (error) {
|
|
@@ -374,13 +481,23 @@ function finalizeLocalArtifact(
|
|
|
374
481
|
return result;
|
|
375
482
|
}
|
|
376
483
|
|
|
484
|
+
/** Validate and seal an immutable preflight generation without publishing the formal artifact. */
|
|
485
|
+
function preflightLocalArtifact(
|
|
486
|
+
request: LocalArtifactFinalizationRequest,
|
|
487
|
+
authority?: LifecycleRecoveryAttestationV1
|
|
488
|
+
): LocalArtifactFinalizationResult {
|
|
489
|
+
const prepared = prepareLocalArtifact(request, undefined, authority, true);
|
|
490
|
+
return prepared.result;
|
|
491
|
+
}
|
|
492
|
+
|
|
377
493
|
export {
|
|
378
494
|
LOCAL_ARTIFACT_REQUIRED_SECTIONS,
|
|
379
495
|
consumeLocalArtifactFinalizationIntent,
|
|
380
496
|
finalizeLocalArtifact,
|
|
497
|
+
preflightLocalArtifact,
|
|
381
498
|
prepareLocalArtifact,
|
|
382
499
|
commitLocalArtifactProvenance,
|
|
383
|
-
|
|
500
|
+
readArtifactRecoveryIntent as readLocalArtifactFinalizationIntent,
|
|
384
501
|
canonicalSemanticDigest as semanticDigest,
|
|
385
502
|
sha256Content,
|
|
386
503
|
validateLocalArtifact
|
|
@@ -394,5 +511,6 @@ export type {
|
|
|
394
511
|
LocalArtifactFinalizationIntent,
|
|
395
512
|
LocalArtifactPreparation,
|
|
396
513
|
LocalArtifactValidationOptions,
|
|
397
|
-
LocalArtifactValidationResult
|
|
514
|
+
LocalArtifactValidationResult,
|
|
515
|
+
LocalArtifactRecoveryInfo
|
|
398
516
|
};
|
|
@@ -68,7 +68,11 @@ function manualValidationReceiptDigest(receipt: ManualValidationReceipt | Manual
|
|
|
68
68
|
|
|
69
69
|
function manualValidationFinalSummaryDigest(body: string): string {
|
|
70
70
|
const heading = /^###\s+✅\s+(?:Manual Validation Passed|人工验证已通过)\s*$/mu.exec(body);
|
|
71
|
-
const
|
|
71
|
+
const afterHeading = heading ? body.slice(heading.index + heading[0].length) : '';
|
|
72
|
+
const nextSection = heading ? /^#{1,3}\s/mu.exec(afterHeading) : null;
|
|
73
|
+
const projection = heading
|
|
74
|
+
? body.slice(heading.index, heading.index + heading[0].length + (nextSection?.index ?? afterHeading.length)).replace(/\s+$/u, '')
|
|
75
|
+
: body;
|
|
72
76
|
const preimage = projection.replace(/receipt=(?:[a-f0-9]{64}|<receipt>)/gu, 'receipt=<receipt>');
|
|
73
77
|
return createHash('sha256').update(preimage, 'utf8').digest('hex');
|
|
74
78
|
}
|