@fitlab-ai/agent-infra 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/internal/cli-route-inventory.js +3 -4
- package/dist/lib/internal/manual-validation.js +6 -2
- 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 +2 -2
- package/dist/lib/internal/task-operation-registry.js +22 -12
- package/dist/lib/internal/task-orchestration.js +2 -2
- package/dist/lib/internal/task-review.js +10 -5
- package/dist/lib/platform/activity-recovery.js +159 -0
- package/dist/lib/platform/issue-comments.js +84 -32
- package/dist/lib/platform/issues.js +3 -4
- package/dist/lib/platform/task-comment-projection.js +70 -0
- package/dist/lib/platform/task-recovery.js +55 -0
- package/dist/lib/platform/verification-sync.js +184 -162
- package/dist/lib/sandbox/control/executor.js +4 -2
- package/dist/lib/sandbox/control/task-workflow.js +1 -1
- package/dist/lib/sandbox/control/workflow-executor.js +90 -22
- package/dist/lib/server/process-state.js +17 -5
- package/dist/lib/task/artifact-command.js +6 -3
- package/dist/lib/task/artifact-lifecycle.js +15 -52
- package/dist/lib/task/artifact-operations.js +28 -2
- package/dist/lib/task/artifact-recovery.js +114 -68
- package/dist/lib/task/artifact-repair-intent.js +25 -8
- package/dist/lib/task/capabilities.js +0 -2
- package/dist/lib/task/control-authority.js +1 -1
- package/dist/lib/task/events.js +66 -49
- package/dist/lib/task/gate-policy.js +94 -0
- package/dist/lib/task/intermediate-cleanup.js +28 -3
- package/dist/lib/task/local-artifact-finalization.js +85 -18
- package/dist/lib/task/orchestration.js +8 -6
- package/dist/lib/task/review-artifacts.js +12 -6
- package/dist/lib/task/review-command.js +2 -2
- package/dist/lib/task/review-finalization.js +198 -11
- package/dist/lib/{host-control → task}/secure-fs.js +8 -19
- package/dist/lib/task/verification-engine.js +28 -17
- package/dist/lib/task/verification.js +25 -9
- package/dist/lib/task/workflow-command.js +2 -0
- package/dist/lib/task/workflow-dispatch.js +161 -0
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +3 -4
- package/lib/internal/manual-validation.ts +6 -2
- 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 +1 -1
- package/lib/internal/task-operation-registry.ts +22 -12
- package/lib/internal/task-orchestration.ts +1 -1
- package/lib/internal/task-review.ts +9 -5
- package/lib/platform/activity-recovery.ts +179 -0
- package/lib/platform/issue-comments.ts +98 -31
- package/lib/platform/issues.ts +3 -4
- package/lib/platform/task-comment-projection.ts +79 -0
- package/lib/platform/task-recovery.ts +54 -0
- package/lib/platform/verification-sync.ts +203 -188
- package/lib/sandbox/control/executor.ts +4 -2
- package/lib/sandbox/control/protocol.ts +1 -1
- package/lib/sandbox/control/task-workflow.ts +1 -1
- package/lib/sandbox/control/workflow-executor.ts +113 -22
- package/lib/server/process-state.ts +26 -5
- package/lib/task/artifact-command.ts +6 -4
- package/lib/task/artifact-lifecycle.ts +12 -49
- package/lib/task/artifact-operations.ts +28 -2
- package/lib/task/artifact-recovery.ts +120 -67
- package/lib/task/artifact-repair-intent.ts +38 -11
- package/lib/task/capabilities.ts +0 -1
- package/lib/task/control-authority.ts +1 -1
- package/lib/task/events.ts +55 -38
- package/lib/task/gate-policy.ts +133 -0
- package/lib/task/intermediate-cleanup.ts +18 -2
- package/lib/task/local-artifact-finalization.ts +91 -15
- package/lib/task/orchestration.ts +12 -7
- package/lib/task/review-artifacts.ts +8 -2
- package/lib/task/review-command.ts +1 -1
- package/lib/task/review-finalization.ts +198 -9
- package/lib/{host-control → task}/secure-fs.ts +10 -32
- package/lib/task/verification-engine.ts +24 -14
- package/lib/task/verification-types.ts +7 -0
- package/lib/task/verification.ts +25 -9
- package/lib/task/workflow-command.ts +2 -0
- package/lib/task/workflow-dispatch.ts +171 -0
- package/package.json +1 -1
- package/templates/.agents/rules/issue-sync.en.md +11 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +6 -2
- package/templates/.agents/rules/local-artifact-repair.en.md +18 -9
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +21 -11
- package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/block-task/config/verify.json +1 -3
- package/templates/.agents/skills/cancel-task/config/verify.json +1 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +11 -0
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +11 -0
- package/templates/.agents/skills/commit/config/verify.json +1 -5
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +6 -6
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +6 -6
- package/templates/.agents/skills/complete-task/config/verify.en.json +3 -14
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +3 -14
- package/templates/.agents/skills/create-pr/config/verify.json +1 -10
- package/templates/.agents/skills/create-task/config/verify.json +1 -4
- package/templates/.agents/skills/import-issue/config/verify.json +1 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/config/verify.en.json +1 -10
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/restore-task/SKILL.en.md +9 -4
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +10 -5
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +3 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +3 -2
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-code/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -10
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -10
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/review-pr/config/verify.en.json +1 -8
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +1 -8
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +38 -13
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +38 -13
- 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
|
@@ -57,8 +57,7 @@ import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead,
|
|
|
57
57
|
import { resolveDeliveryTarget, resolveDiffBase, resolveTargetHead } from './delivery-target.ts';
|
|
58
58
|
import { buildLifecycleFacts, canStart, recommendNext } from './capabilities.ts';
|
|
59
59
|
import { hasOpenLifecycleExecution } from './activity-log.ts';
|
|
60
|
-
import {
|
|
61
|
-
import type { InvalidationDocument } from './invalidation.ts';
|
|
60
|
+
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
62
61
|
import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
|
|
63
62
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
64
63
|
|
|
@@ -678,13 +677,11 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
678
677
|
}
|
|
679
678
|
let metadata: ReturnType<typeof parseTypedTaskFrontmatter>;
|
|
680
679
|
let currentLedger: LedgerDocument | null = null;
|
|
681
|
-
let currentInvalidation: InvalidationDocument = { operations: [], targets: [] };
|
|
682
680
|
if (resolved.state === 'active') {
|
|
683
681
|
const contract = validateCurrentTaskContract(content);
|
|
684
682
|
if (!contract.ok) return failed('ORCHESTRATION_CURRENT_CONTRACT_INVALID', contract.message, resolved.taskId);
|
|
685
683
|
metadata = contract.metadata;
|
|
686
684
|
currentLedger = contract.ledger;
|
|
687
|
-
currentInvalidation = contract.invalidation;
|
|
688
685
|
} else {
|
|
689
686
|
try {
|
|
690
687
|
metadata = parseTypedTaskFrontmatter(content);
|
|
@@ -692,9 +689,6 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
692
689
|
return failed('ORCHESTRATION_TASK_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
693
690
|
}
|
|
694
691
|
}
|
|
695
|
-
if (invalidationBlocks(currentInvalidation)) {
|
|
696
|
-
return failed('ORCHESTRATION_INVALIDATION_INCOMPLETE', 'artifact invalidation is incomplete; reconcile task.md before routing downstream work', resolved.taskId);
|
|
697
|
-
}
|
|
698
692
|
let run: OrchestrationRun | null;
|
|
699
693
|
try {
|
|
700
694
|
run = readRun(resolved.taskDir, options);
|
|
@@ -711,6 +705,9 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
711
705
|
const routed = routeFromFacts(facts.facts);
|
|
712
706
|
if (!routed) return failed('ORCHESTRATION_ROUTE_UNKNOWN', 'cannot determine a unique lifecycle action', resolved.taskId);
|
|
713
707
|
if ('completion' in routed) {
|
|
708
|
+
if (facts.facts.executionBusy && !run?.pendingDelegation) {
|
|
709
|
+
return failed('ORCHESTRATION_EXECUTION_BUSY', 'clean completion requires no open lifecycle execution', resolved.taskId);
|
|
710
|
+
}
|
|
714
711
|
const reviewRound = maxArtifactRound(fs.readdirSync(resolved.taskDir), 'review-code');
|
|
715
712
|
const review = parseReviewSummary(fs.readFileSync(
|
|
716
713
|
path.join(resolved.taskDir, artifactName('review-code', reviewRound)),
|
|
@@ -847,6 +844,14 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
847
844
|
return failed('ORCHESTRATION_DELEGATION_BUSY', 'the repository already has a pending lifecycle delegation', resolved.taskId);
|
|
848
845
|
}
|
|
849
846
|
if (run.stepCount >= run.maxSteps) return pauseOrchestration(taskRef, 'ORCHESTRATION_MAX_STEPS', 'maximum orchestration steps reached', true, options);
|
|
847
|
+
const invalidation = reconcileTaskInvalidation(taskRef, { repoRoot: resolved.repoRoot });
|
|
848
|
+
if (invalidation.status === 'failed') {
|
|
849
|
+
return failed(
|
|
850
|
+
'ORCHESTRATION_INVALIDATION_RECONCILE_FAILED',
|
|
851
|
+
`${invalidation.error?.code ?? 'UNKNOWN'}: ${invalidation.error?.message ?? ''}`,
|
|
852
|
+
resolved.taskId
|
|
853
|
+
);
|
|
854
|
+
}
|
|
850
855
|
const routed = routeOrchestration(taskRef, options);
|
|
851
856
|
if (!routed.next) return routed;
|
|
852
857
|
const next = routed.next;
|
|
@@ -176,19 +176,25 @@ function equalCounts(left: ReviewFindingCounts, right: ReviewFindingCounts): boo
|
|
|
176
176
|
|
|
177
177
|
function finalizeReviewSummaryContent(
|
|
178
178
|
content: string,
|
|
179
|
-
counts: ReviewFindingCounts
|
|
179
|
+
counts: ReviewFindingCounts,
|
|
180
|
+
options: { refreshNumericCounts?: boolean } = {}
|
|
180
181
|
): ReviewSummaryFinalizeResult {
|
|
181
182
|
const parsed = parseReviewSummary(content);
|
|
182
183
|
if (!parsed.ok) return parsed;
|
|
183
184
|
if (parsed.summary.countState === 'numeric') {
|
|
184
185
|
if (equalCounts(parsed.summary.counts!, counts)) return { ok: true, changed: false, content };
|
|
185
|
-
return {
|
|
186
|
+
if (!options.refreshNumericCounts) return {
|
|
186
187
|
ok: false,
|
|
187
188
|
code: 'REVIEW_SUMMARY_COUNT_MISMATCH',
|
|
188
189
|
message: 'review summary finding counts do not match the ledger snapshot'
|
|
189
190
|
};
|
|
190
191
|
}
|
|
191
192
|
let line = content.slice(parsed.findingsStart, parsed.findingsEnd);
|
|
193
|
+
if (parsed.summary.countState === 'numeric') {
|
|
194
|
+
const values = [counts.blocker, counts.major, counts.minor];
|
|
195
|
+
let ordinal = 0;
|
|
196
|
+
line = line.replace(/\d+(?=\s*(?:阻塞项|主要|次要|blockers?\b|majors?\b|minors?\b))/gi, () => String(values[ordinal++]));
|
|
197
|
+
}
|
|
192
198
|
line = line
|
|
193
199
|
.replace(PLACEHOLDERS[0], String(counts.blocker))
|
|
194
200
|
.replace(PLACEHOLDERS[1], String(counts.major))
|
|
@@ -3,7 +3,7 @@ import type { ReviewFinalizationRequest } from './review-finalization.ts';
|
|
|
3
3
|
export function parseReviewCommand(args: readonly string[]): ReviewFinalizationRequest & {
|
|
4
4
|
overrideTicket?: string; overrideTarget?: string; overrideScope?: string;
|
|
5
5
|
} {
|
|
6
|
-
if (!args[0] || args[1]
|
|
6
|
+
if (!args[0] || !['preflight', 'finalize-summary'].includes(args[1] ?? '')) throw new Error('task ref and a supported review operation are required');
|
|
7
7
|
const values: Record<string, string | boolean> = {};
|
|
8
8
|
const flags = {
|
|
9
9
|
'--stage': 'stage', '--artifact': 'artifact', '--orchestrated': 'orchestrated', '--dry-run': 'dryRun',
|
|
@@ -14,11 +14,14 @@ import { allowsManualOverride } from './guard-override.ts';
|
|
|
14
14
|
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
15
15
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
16
16
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
17
|
+
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
17
18
|
import { readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
18
19
|
import {
|
|
19
20
|
beginArtifactRecovery,
|
|
20
21
|
commitArtifactRecovery,
|
|
22
|
+
prepareArtifactRecoveryFinal,
|
|
21
23
|
prepareArtifactRecoveryCommit,
|
|
24
|
+
reconcileArtifactRecovery,
|
|
22
25
|
recoveryContextFromIntent,
|
|
23
26
|
stageArtifactCandidate
|
|
24
27
|
} from './artifact-recovery.ts';
|
|
@@ -36,6 +39,7 @@ type ReviewFinalizationErrorCode =
|
|
|
36
39
|
| 'REVIEW_SUMMARY_PLACEHOLDER_INVALID'
|
|
37
40
|
| 'REVIEW_SUMMARY_COUNT_MISMATCH'
|
|
38
41
|
| 'REVIEW_ARTIFACT_STRUCTURE_INVALID'
|
|
42
|
+
| 'REVIEW_ARTIFACT_QUALIFICATION_INVALID'
|
|
39
43
|
| 'REVIEW_DECISION_DETAIL_INVALID'
|
|
40
44
|
| 'REVIEW_ARTIFACT_CONFLICT'
|
|
41
45
|
| 'REVIEW_PROVENANCE_INVALID'
|
|
@@ -73,6 +77,11 @@ type ReviewFinalizationResult = {
|
|
|
73
77
|
}[];
|
|
74
78
|
error: ReviewFinalizationError | null;
|
|
75
79
|
};
|
|
80
|
+
type ReviewPreflightResult = Omit<ReviewFinalizationResult, 'status' | 'intent' | 'stageStatus'> & {
|
|
81
|
+
status: 'passed' | 'failed';
|
|
82
|
+
intent: 'preflight';
|
|
83
|
+
stageStatus: null;
|
|
84
|
+
};
|
|
76
85
|
type ReviewFileSystem = {
|
|
77
86
|
readFileSync: (file: string) => string;
|
|
78
87
|
};
|
|
@@ -118,6 +127,141 @@ function failed(
|
|
|
118
127
|
};
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
function preflightFailed(
|
|
131
|
+
request: ReviewFinalizationRequest,
|
|
132
|
+
code: ReviewFinalizationErrorCode,
|
|
133
|
+
message: string,
|
|
134
|
+
taskId: string | null = null,
|
|
135
|
+
extra: Partial<ReviewPreflightResult> = {}
|
|
136
|
+
): ReviewPreflightResult {
|
|
137
|
+
const { status: _status, intent: _intent, stageStatus: _stageStatus, ...finalizationExtra } = extra;
|
|
138
|
+
return {
|
|
139
|
+
...failed(request, code, message, taskId, null, finalizationExtra),
|
|
140
|
+
status: 'failed',
|
|
141
|
+
intent: 'preflight',
|
|
142
|
+
stageStatus: null
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Validate and seal a review draft before any finding-ledger write. */
|
|
147
|
+
function preflightReviewSummaryUnlocked(
|
|
148
|
+
request: ReviewFinalizationRequest,
|
|
149
|
+
options: ReviewFinalizationOptions = {}
|
|
150
|
+
): ReviewPreflightResult {
|
|
151
|
+
const stage = request.stage as ReviewStage;
|
|
152
|
+
const spec = STAGES[stage];
|
|
153
|
+
if (!spec) return preflightFailed(request, 'REVIEW_STAGE_INVALID', `unsupported review stage '${request.stage}'`);
|
|
154
|
+
if (!request.taskRef || !request.artifact) return preflightFailed(request, 'REVIEW_PAYLOAD_INVALID', 'taskRef, stage, and artifact are required');
|
|
155
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
156
|
+
if (!resolved.ok) return preflightFailed(request, resolved.code, resolved.message, resolved.taskId ?? null);
|
|
157
|
+
if (resolved.state !== 'active') return preflightFailed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
158
|
+
const parsed = parseArtifactName(request.artifact);
|
|
159
|
+
if (!parsed || parsed.family !== spec.family) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${spec.family}`, resolved.taskId);
|
|
160
|
+
const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
|
|
161
|
+
if (publicationError) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
|
|
162
|
+
|
|
163
|
+
let taskContent: string;
|
|
164
|
+
let content: string;
|
|
165
|
+
let recovery: ArtifactRecoveryContext | undefined;
|
|
166
|
+
try {
|
|
167
|
+
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
168
|
+
if (request.recoveryId) {
|
|
169
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
170
|
+
if (!intent || intent.recoveryOperationId !== request.recoveryId) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal', resolved.taskId);
|
|
171
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready'].includes(intent.state)) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`, resolved.taskId);
|
|
172
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
173
|
+
content = fs.existsSync(recovery.stagingPath)
|
|
174
|
+
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
175
|
+
: fs.readFileSync(recovery.formalPath, 'utf8');
|
|
176
|
+
} else {
|
|
177
|
+
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
178
|
+
if (!validated.ok) return preflightFailed(request, validated.error.code === 'ARTIFACT_NOT_REGULAR' ? 'REVIEW_ARTIFACT_NOT_REGULAR' : 'REVIEW_ARTIFACT_IDENTITY_INVALID', validated.error.message, resolved.taskId);
|
|
179
|
+
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
180
|
+
const intent = readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact);
|
|
181
|
+
if (intent?.state === 'preflight-ready') recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
182
|
+
if (intent?.state === 'consumed') return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', 'review artifact was already finalized', resolved.taskId);
|
|
183
|
+
}
|
|
184
|
+
} catch (error) { return preflightFailed(request, 'REVIEW_ARTIFACT_NOT_REGULAR', String(error), resolved.taskId); }
|
|
185
|
+
|
|
186
|
+
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`, resolved.taskId);
|
|
187
|
+
const execution = validateLifecycleExecution(request.taskRef, {
|
|
188
|
+
mode: request.orchestrated ? 'orchestrated' : 'standalone',
|
|
189
|
+
identity: { stage: spec.family, round: parsed.round, artifact: request.artifact, role: 'reviewer' },
|
|
190
|
+
dryRun: request.dryRun
|
|
191
|
+
}, { repoRoot: options.repoRoot });
|
|
192
|
+
if (!execution.ok) return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${execution.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH'}: ${execution.error?.message ?? 'orchestration provenance validation failed'}`, resolved.taskId);
|
|
193
|
+
|
|
194
|
+
const artifactSha256 = sha256Content(content);
|
|
195
|
+
const semanticDigest = canonicalSemanticDigest(content);
|
|
196
|
+
const detail = inspectDecisionDetailDuplicates(content);
|
|
197
|
+
const structure = inspectArtifactContract(content, getArtifactSchema(spec.family)!);
|
|
198
|
+
const expected = expectedQualificationRelations(taskContent, spec.family);
|
|
199
|
+
if (!expected.ok) return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
|
|
200
|
+
const qualification = validateQualificationAudit(taskContent, content, {
|
|
201
|
+
family: spec.family,
|
|
202
|
+
artifact: request.artifact,
|
|
203
|
+
expectedUpstreamRelations: expected.relations
|
|
204
|
+
});
|
|
205
|
+
const qualificationError = qualification.ok ? null : qualification;
|
|
206
|
+
if (!detail.ok || !structure.ok || qualificationError) {
|
|
207
|
+
if (!recovery && !request.dryRun) {
|
|
208
|
+
try {
|
|
209
|
+
recovery = beginArtifactRecovery({
|
|
210
|
+
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
211
|
+
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
212
|
+
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
213
|
+
} catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId); }
|
|
214
|
+
}
|
|
215
|
+
const diagnostics = [
|
|
216
|
+
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
217
|
+
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`),
|
|
218
|
+
...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
|
|
219
|
+
].join('; ');
|
|
220
|
+
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : !structure.ok ? 'REVIEW_ARTIFACT_STRUCTURE_INVALID' : 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', diagnostics, resolved.taskId, {
|
|
221
|
+
artifactSha256, semanticDigest,
|
|
222
|
+
...(recovery ? { recovery: recoveryInfo(recovery) } : {})
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (request.dryRun || recovery && readArtifactRecoveryIntent(resolved.repoRoot, resolved.taskId, spec.family, request.artifact)?.state === 'preflight-ready') {
|
|
226
|
+
return {
|
|
227
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, ...(recovery ? { recovery: recoveryInfo(recovery) } : {}) }),
|
|
228
|
+
status: 'passed', changed: false, error: null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
recovery ??= beginArtifactRecovery({
|
|
233
|
+
taskId: resolved.taskId, family: spec.family, artifact: request.artifact, round: parsed.round,
|
|
234
|
+
requestId: `review-preflight:${resolved.taskId}:${stage}:${parsed.round}`
|
|
235
|
+
}, Buffer.from(content, 'utf8'), { repoRoot: resolved.repoRoot, taskDir: resolved.taskDir, lockAlreadyHeld: options.lockAlreadyHeld });
|
|
236
|
+
const staged = stageArtifactCandidate(recovery, Buffer.from(content, 'utf8'), { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
237
|
+
prepareArtifactRecoveryCommit(recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: options.lockAlreadyHeld });
|
|
238
|
+
return {
|
|
239
|
+
...preflightFailed(request, 'REVIEW_ARTIFACT_CONFLICT', '', resolved.taskId, { artifactSha256, semanticDigest, recovery: recoveryInfo(recovery) }),
|
|
240
|
+
status: 'passed', changed: false, error: null
|
|
241
|
+
};
|
|
242
|
+
} catch (error) { return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', String(error), resolved.taskId, { artifactSha256, semanticDigest }); }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function preflightReviewSummary(
|
|
246
|
+
request: ReviewFinalizationRequest,
|
|
247
|
+
options: ReviewFinalizationOptions = {}
|
|
248
|
+
): ReviewPreflightResult {
|
|
249
|
+
if (request.dryRun || options.lockAlreadyHeld) return preflightReviewSummaryUnlocked(request, options);
|
|
250
|
+
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
251
|
+
if (!resolved.ok) return preflightReviewSummaryUnlocked(request, options);
|
|
252
|
+
try {
|
|
253
|
+
return withTaskExecutionLock(
|
|
254
|
+
resolved.repoRoot,
|
|
255
|
+
resolved.taskId,
|
|
256
|
+
'task-review.preflight',
|
|
257
|
+
() => preflightReviewSummaryUnlocked(request, { ...options, lockAlreadyHeld: true })
|
|
258
|
+
);
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (!(error instanceof TaskExecutionLockError)) throw error;
|
|
261
|
+
return preflightFailed(request, 'REVIEW_PROVENANCE_INVALID', `${error.code}: ${error.message}`, resolved.taskId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
121
265
|
type ReviewSummaryCandidatePreparation = Readonly<{
|
|
122
266
|
result: ReviewFinalizationResult;
|
|
123
267
|
content: string;
|
|
@@ -125,6 +269,15 @@ type ReviewSummaryCandidatePreparation = Readonly<{
|
|
|
125
269
|
lockAlreadyHeld?: boolean;
|
|
126
270
|
}>;
|
|
127
271
|
|
|
272
|
+
function recoveryInfo(recovery: ArtifactRecoveryContext): NonNullable<ReviewFinalizationResult['recovery']> {
|
|
273
|
+
return {
|
|
274
|
+
recoveryId: recovery.recoveryId,
|
|
275
|
+
candidatePath: recovery.stagingPath,
|
|
276
|
+
baselineSha256: recovery.baselineSha256,
|
|
277
|
+
baselineSemanticDigest: recovery.baselineSemanticDigest
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
128
281
|
/** Prepare domain validation, summary bytes and provenance before any publication. */
|
|
129
282
|
function prepareReviewSummaryCandidate(
|
|
130
283
|
request: ReviewFinalizationRequest,
|
|
@@ -158,7 +311,7 @@ function prepareReviewSummaryCandidate(
|
|
|
158
311
|
try {
|
|
159
312
|
const intent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact);
|
|
160
313
|
if (!intent || intent.recoveryOperationId !== request.recoveryId) return reject('REVIEW_PROVENANCE_INVALID', 'recovery id does not match the artifact journal');
|
|
161
|
-
if (!['awaiting-recovery', '
|
|
314
|
+
if (!['awaiting-preflight-recovery', 'preflight-ready', 'preflight-commit-started', 'preflight-passed', 'full-finalizer-ready', 'commit-started'].includes(intent.state)) return reject('REVIEW_PROVENANCE_INVALID', `recovery journal is in '${intent.state}' state`);
|
|
162
315
|
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, intent);
|
|
163
316
|
artifactContent = fs.existsSync(recovery.stagingPath)
|
|
164
317
|
? fs.readFileSync(recovery.stagingPath, 'utf8')
|
|
@@ -171,11 +324,15 @@ function prepareReviewSummaryCandidate(
|
|
|
171
324
|
let repairIntent;
|
|
172
325
|
try { repairIntent = readArtifactRecoveryIntent(resolved.repoRoot, taskId!, spec.family, request.artifact); }
|
|
173
326
|
catch (error) { return reject('REVIEW_PROVENANCE_INVALID', String(error), digests); }
|
|
174
|
-
if (repairIntent?.state === '
|
|
327
|
+
if (repairIntent?.state === 'consumed') {
|
|
175
328
|
if (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest) {
|
|
176
329
|
return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact changed after its finalization provenance was recorded', digests);
|
|
177
330
|
}
|
|
178
331
|
}
|
|
332
|
+
if (repairIntent?.state === 'passed'
|
|
333
|
+
&& (repairIntent.finalArtifactSha256 !== artifactSha256 || repairIntent.finalSemanticDigest !== semanticDigest)) {
|
|
334
|
+
repairIntent = undefined;
|
|
335
|
+
}
|
|
179
336
|
if (!hasOpenArtifactRound(taskContent, spec.family, parsed.round)) {
|
|
180
337
|
return reject('REVIEW_ARTIFACT_IDENTITY_INVALID', `${request.artifact} does not have one matching open started review event`);
|
|
181
338
|
}
|
|
@@ -220,9 +377,17 @@ function prepareReviewSummaryCandidate(
|
|
|
220
377
|
...(recovery ? { recovery } : {})
|
|
221
378
|
};
|
|
222
379
|
}
|
|
223
|
-
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts
|
|
380
|
+
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
|
|
381
|
+
refreshNumericCounts: Boolean(request.recoveryId && recovery)
|
|
382
|
+
});
|
|
224
383
|
if (!transformed.ok) return reject(transformed.code, transformed.message);
|
|
225
384
|
const finalDigests = { artifactSha256: sha256Content(transformed.content), semanticDigest: canonicalSemanticDigest(transformed.content) };
|
|
385
|
+
if (!recovery && repairIntent?.state === 'commit-started') {
|
|
386
|
+
if (repairIntent.finalArtifactSha256 !== finalDigests.artifactSha256 || repairIntent.finalSemanticDigest !== finalDigests.semanticDigest) {
|
|
387
|
+
return reject('REVIEW_PROVENANCE_INVALID', 'formal review artifact does not match the interrupted recovery journal', finalDigests);
|
|
388
|
+
}
|
|
389
|
+
recovery = recoveryContextFromIntent(resolved.repoRoot, resolved.taskDir, repairIntent);
|
|
390
|
+
}
|
|
226
391
|
if (!recovery && (repairIntent?.state === 'passed' || repairIntent?.state === 'consumed')) {
|
|
227
392
|
if (transformed.changed) return reject('REVIEW_PROVENANCE_INVALID', 'the review artifact requires changes after its completed recovery was consumed', finalDigests);
|
|
228
393
|
return {
|
|
@@ -260,21 +425,43 @@ function prepareReviewSummaryCandidate(
|
|
|
260
425
|
|
|
261
426
|
function commitReviewSummaryProvenance(
|
|
262
427
|
prepared: ReviewSummaryCandidatePreparation,
|
|
263
|
-
repoRoot: string
|
|
428
|
+
repoRoot: string,
|
|
429
|
+
options: Readonly<{ afterPublish?: () => void }> = {}
|
|
264
430
|
): ReviewFinalizationResult {
|
|
265
431
|
if (prepared.result.status === 'failed' || !prepared.recovery) return prepared.result;
|
|
266
432
|
try {
|
|
267
433
|
const intent = readArtifactRecoveryIntent(prepared.recovery.repoRoot, prepared.recovery.taskId, prepared.recovery.family, prepared.recovery.artifact);
|
|
268
434
|
if (!intent) throw new Error('ARTIFACT_RECOVERY_INTENT_MISSING');
|
|
269
435
|
let committed;
|
|
270
|
-
if (intent.state === '
|
|
271
|
-
|
|
272
|
-
|
|
436
|
+
if (intent.state === 'passed' || intent.state === 'consumed') {
|
|
437
|
+
return {
|
|
438
|
+
...prepared.result,
|
|
439
|
+
status: 'no-op',
|
|
440
|
+
changed: false,
|
|
441
|
+
artifactSha256: intent.finalArtifactSha256,
|
|
442
|
+
semanticDigest: intent.finalSemanticDigest,
|
|
443
|
+
error: null
|
|
444
|
+
};
|
|
445
|
+
} else if (intent.state === 'commit-started') {
|
|
446
|
+
const reconciled = reconcileArtifactRecovery(prepared.recovery, {
|
|
447
|
+
lockAlreadyHeld: prepared.lockAlreadyHeld,
|
|
448
|
+
afterPublish: options.afterPublish
|
|
449
|
+
});
|
|
450
|
+
committed = reconciled.intent;
|
|
451
|
+
} else if (intent.state === 'awaiting-preflight-recovery') {
|
|
273
452
|
const staged = stageArtifactCandidate(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
274
453
|
prepareArtifactRecoveryCommit(prepared.recovery, staged.candidateSha256, staged.semanticDigest, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
275
454
|
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
455
|
+
} else if (intent.state === 'preflight-ready' || intent.state === 'preflight-commit-started') {
|
|
456
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
457
|
+
}
|
|
458
|
+
if (committed?.state === 'preflight-passed' || intent.state === 'preflight-passed') {
|
|
459
|
+
prepareArtifactRecoveryFinal(prepared.recovery, Buffer.from(prepared.content, 'utf8'), { lockAlreadyHeld: prepared.lockAlreadyHeld });
|
|
460
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
461
|
+
} else if (intent.state === 'full-finalizer-ready') {
|
|
462
|
+
committed = commitArtifactRecovery(prepared.recovery, { lockAlreadyHeld: prepared.lockAlreadyHeld, afterPublish: options.afterPublish });
|
|
276
463
|
}
|
|
277
|
-
if (committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed.state}'`);
|
|
464
|
+
if (!committed || committed.state !== 'passed') throw new Error(`ARTIFACT_RECOVERY_STATE_INVALID: commit ended in '${committed?.state ?? intent.state}'`);
|
|
278
465
|
return {
|
|
279
466
|
...prepared.result,
|
|
280
467
|
artifactSha256: committed.finalArtifactSha256,
|
|
@@ -282,6 +469,7 @@ function commitReviewSummaryProvenance(
|
|
|
282
469
|
error: null
|
|
283
470
|
};
|
|
284
471
|
} catch (error) {
|
|
472
|
+
if (error instanceof Error && error.message.startsWith('TASK_WORKFLOW_FAULT_INJECTED:')) throw error;
|
|
285
473
|
return { ...prepared.result, status: 'failed', error: {
|
|
286
474
|
code: 'REVIEW_RECOVERY_COMMIT_FAILED', message: `cannot publish review candidate: ${String(error)}`
|
|
287
475
|
} };
|
|
@@ -335,12 +523,13 @@ function finalizeReviewSummary(
|
|
|
335
523
|
}
|
|
336
524
|
}
|
|
337
525
|
|
|
338
|
-
export { finalizeReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
|
|
526
|
+
export { finalizeReviewSummary, preflightReviewSummary, prepareReviewSummaryCandidate, commitReviewSummaryProvenance };
|
|
339
527
|
export type {
|
|
340
528
|
ReviewFinalizationError,
|
|
341
529
|
ReviewFinalizationErrorCode,
|
|
342
530
|
ReviewFinalizationOptions,
|
|
343
531
|
ReviewFinalizationRequest,
|
|
344
532
|
ReviewFinalizationResult,
|
|
533
|
+
ReviewPreflightResult,
|
|
345
534
|
ReviewSummaryCandidatePreparation
|
|
346
535
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
4
4
|
|
|
5
5
|
const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
|
|
6
6
|
const DIRECTORY = fs.constants.O_DIRECTORY ?? 0;
|
|
@@ -25,11 +25,8 @@ function conflict(message: string): never {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function sameStat(left: fs.BigIntStats, right: fs.BigIntStats): boolean {
|
|
28
|
-
return left.dev === right.dev
|
|
29
|
-
&& left.
|
|
30
|
-
&& left.size === right.size
|
|
31
|
-
&& left.mtimeNs === right.mtimeNs
|
|
32
|
-
&& left.ctimeNs === right.ctimeNs;
|
|
28
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
29
|
+
&& left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
function assertNoSymlinkAncestors(candidate: string): void {
|
|
@@ -43,25 +40,15 @@ function assertNoSymlinkAncestors(candidate: string): void {
|
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
export async function readStableFile(
|
|
47
|
-
candidate: string,
|
|
48
|
-
options: Readonly<{ maxBytes: number; expectedSha256?: string }>
|
|
49
|
-
): Promise<StableFile> {
|
|
43
|
+
export async function readStableFile(candidate: string, options: Readonly<{ maxBytes: number; expectedSha256?: string }>): Promise<StableFile> {
|
|
50
44
|
return readStableFileSync(candidate, options);
|
|
51
45
|
}
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
candidate: string,
|
|
56
|
-
options: Readonly<{ maxBytes: number; expectedSha256?: string }>
|
|
57
|
-
): StableFile {
|
|
58
|
-
if (!path.isAbsolute(candidate)) {
|
|
59
|
-
conflict('candidate path must be absolute and terminal');
|
|
60
|
-
}
|
|
47
|
+
export function readStableFileSync(candidate: string, options: Readonly<{ maxBytes: number; expectedSha256?: string }>): StableFile {
|
|
48
|
+
if (!path.isAbsolute(candidate)) conflict('candidate path must be absolute and terminal');
|
|
61
49
|
assertNoSymlinkAncestors(candidate);
|
|
62
50
|
let descriptor: number | undefined;
|
|
63
51
|
try {
|
|
64
|
-
// O_NONBLOCK lets fstat reject a FIFO even when it has no writer.
|
|
65
52
|
descriptor = fs.openSync(candidate, fs.constants.O_RDONLY | NO_FOLLOW | (fs.constants.O_NONBLOCK ?? 0));
|
|
66
53
|
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
67
54
|
if (!before.isFile()) conflict('candidate is not a regular file');
|
|
@@ -76,16 +63,12 @@ export function readStableFileSync(
|
|
|
76
63
|
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
77
64
|
if (!sameStat(before, after)) conflict('candidate identity or metadata changed during read');
|
|
78
65
|
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
79
|
-
if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256)
|
|
80
|
-
conflict('candidate digest does not match the expected digest');
|
|
81
|
-
}
|
|
66
|
+
if (options.expectedSha256 !== undefined && sha256 !== options.expectedSha256) conflict('candidate digest does not match the expected digest');
|
|
82
67
|
return { bytes, sha256, stat: after };
|
|
83
68
|
} catch (error) {
|
|
84
69
|
if (error instanceof SecureFileError) throw error;
|
|
85
70
|
const code = (error as NodeJS.ErrnoException).code;
|
|
86
|
-
if (code === 'ELOOP' || code === 'ENOTSUP' || code === 'EINVAL')
|
|
87
|
-
conflict('candidate terminal is not a stable non-symlink file');
|
|
88
|
-
}
|
|
71
|
+
if (code === 'ELOOP' || code === 'ENOTSUP' || code === 'EINVAL') conflict('candidate terminal is not a stable non-symlink file');
|
|
89
72
|
throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', error instanceof Error ? error.message : String(error));
|
|
90
73
|
} finally {
|
|
91
74
|
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
@@ -99,18 +82,14 @@ export async function writeAtomicFile(target: string, bytes: Buffer, mode = 0o60
|
|
|
99
82
|
let stat: fs.Stats;
|
|
100
83
|
try { stat = await fs.promises.lstat(current); }
|
|
101
84
|
catch { throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is missing'); }
|
|
102
|
-
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
103
|
-
throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is not a real directory');
|
|
104
|
-
}
|
|
85
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative parent directory is not a real directory');
|
|
105
86
|
const next = path.dirname(current);
|
|
106
87
|
if (next === current) break;
|
|
107
88
|
current = next;
|
|
108
89
|
}
|
|
109
90
|
try {
|
|
110
91
|
const existing = await fs.promises.lstat(target);
|
|
111
|
-
if (existing.isSymbolicLink() || !existing.isFile())
|
|
112
|
-
throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative target is not a regular file');
|
|
113
|
-
}
|
|
92
|
+
if (existing.isSymbolicLink() || !existing.isFile()) throw new SecureFileError('TASK_ARTIFACT_WRITE_CONFLICT', 'authoritative target is not a regular file');
|
|
114
93
|
} catch (error) {
|
|
115
94
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
116
95
|
}
|
|
@@ -123,7 +102,6 @@ export async function writeAtomicFile(target: string, bytes: Buffer, mode = 0o60
|
|
|
123
102
|
await handle.chmod(mode);
|
|
124
103
|
await handle.close();
|
|
125
104
|
handle = undefined;
|
|
126
|
-
// Check after all asynchronous preparation; keep the final check/rename adjacent.
|
|
127
105
|
if (expectedSha256 !== undefined) readStableFileSync(target, { maxBytes: 1024 * 1024, expectedSha256 });
|
|
128
106
|
fs.renameSync(temporary, target);
|
|
129
107
|
const directory = await fs.promises.open(parent, fs.constants.O_RDONLY | DIRECTORY);
|
|
@@ -44,6 +44,7 @@ import { validateQualificationAudit } from "./qualification-audit.ts";
|
|
|
44
44
|
import { getArtifactSchema } from "./artifact-schema.ts";
|
|
45
45
|
import { inspectArtifactContract } from "./artifact-operations.ts";
|
|
46
46
|
import type { VerificationShared } from "./verification-types.ts";
|
|
47
|
+
import { normalizeVerificationRecord } from './gate-policy.ts';
|
|
47
48
|
import { manualValidationFinalSummaryProjectionMatches } from "./manual-validation-receipt.ts";
|
|
48
49
|
import { readManualValidationCompletion } from "./manual-validation-completion.ts";
|
|
49
50
|
import { sha256File } from "./artifact-receipts.ts";
|
|
@@ -1392,11 +1393,11 @@ function interpolate(template: any, taskDir: any, artifactFile: any): any {
|
|
|
1392
1393
|
}
|
|
1393
1394
|
|
|
1394
1395
|
function summarizeGate(checks: any): any {
|
|
1395
|
-
if (checks.some((check: any) => check.
|
|
1396
|
+
if (checks.some((check: any) => check.effectiveStatus === "blocked")) {
|
|
1396
1397
|
return "blocked";
|
|
1397
1398
|
}
|
|
1398
1399
|
|
|
1399
|
-
if (checks.some((check: any) => check.
|
|
1400
|
+
if (checks.some((check: any) => check.effectiveStatus === "fail")) {
|
|
1400
1401
|
return "fail";
|
|
1401
1402
|
}
|
|
1402
1403
|
|
|
@@ -1405,9 +1406,9 @@ function summarizeGate(checks: any): any {
|
|
|
1405
1406
|
|
|
1406
1407
|
function summarizeChecks(checks: any): any {
|
|
1407
1408
|
const counts = {
|
|
1408
|
-
pass: checks.filter((check: any) => check.
|
|
1409
|
-
fail: checks.filter((check: any) => check.
|
|
1410
|
-
blocked: checks.filter((check: any) => check.
|
|
1409
|
+
pass: checks.filter((check: any) => check.effectiveStatus === "pass").length,
|
|
1410
|
+
fail: checks.filter((check: any) => check.effectiveStatus === "fail").length,
|
|
1411
|
+
blocked: checks.filter((check: any) => check.effectiveStatus === "blocked").length
|
|
1411
1412
|
};
|
|
1412
1413
|
|
|
1413
1414
|
if (counts.blocked > 0) {
|
|
@@ -1422,7 +1423,7 @@ function buildAction(gate: any, checks: any): any {
|
|
|
1422
1423
|
return "All declared checks passed";
|
|
1423
1424
|
}
|
|
1424
1425
|
|
|
1425
|
-
const firstFailure = checks.find((check: any) => check.
|
|
1426
|
+
const firstFailure = checks.find((check: any) => check.effectiveStatus !== "pass");
|
|
1426
1427
|
if (!firstFailure) {
|
|
1427
1428
|
return "Review validation output";
|
|
1428
1429
|
}
|
|
@@ -1484,16 +1485,16 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
|
|
|
1484
1485
|
const checks = [];
|
|
1485
1486
|
for (const [type, checkConfig] of Object.entries(verifyConfig.checks || {})) {
|
|
1486
1487
|
if (checkConfig === null) continue;
|
|
1487
|
-
const result = await runCheck(type, {
|
|
1488
|
+
const result = normalizeVerificationRecord(await runCheck(type, {
|
|
1488
1489
|
mode,
|
|
1489
1490
|
skillName,
|
|
1490
1491
|
taskDir: path.resolve(taskDir),
|
|
1491
1492
|
artifactFile,
|
|
1492
1493
|
repositoryRoot: repoRoot,
|
|
1493
1494
|
config: checkConfig
|
|
1494
|
-
}, shared);
|
|
1495
|
-
checks.push(result);
|
|
1496
|
-
if (
|
|
1495
|
+
}, shared));
|
|
1496
|
+
checks.push(...(result.subchecks?.map(normalizeVerificationRecord) ?? [result]));
|
|
1497
|
+
if (checks.at(-1)?.effectiveStatus === "blocked") break;
|
|
1497
1498
|
}
|
|
1498
1499
|
const gate = summarizeGate(checks);
|
|
1499
1500
|
return { gate, skill: skillName, checks, summary: summarizeChecks(checks), action: buildAction(gate, checks) };
|
|
@@ -1502,15 +1503,24 @@ async function verifyInProcess({ mode, skillName, taskDir, artifactFile, checks:
|
|
|
1502
1503
|
const config = (verifyConfig.checks || {})[type];
|
|
1503
1504
|
if (config === undefined) return { skill: skillName, ...failResult(type, `Unknown check type '${type}' for skill '${skillName}'.`) };
|
|
1504
1505
|
if (config === null) return { skill: skillName, ...passResult(type, `Check '${type}' is disabled for skill '${skillName}'.`) };
|
|
1505
|
-
|
|
1506
|
-
skill: skillName,
|
|
1507
|
-
...await runCheck(type, {
|
|
1506
|
+
const result = normalizeVerificationRecord(await runCheck(type, {
|
|
1508
1507
|
skillName,
|
|
1509
1508
|
taskDir: path.resolve(taskDir),
|
|
1510
1509
|
artifactFile,
|
|
1511
1510
|
repositoryRoot: repoRoot,
|
|
1512
1511
|
config
|
|
1513
|
-
}, shared)
|
|
1512
|
+
}, shared));
|
|
1513
|
+
if (!result.subchecks?.length) return { skill: skillName, ...result };
|
|
1514
|
+
const checks = result.subchecks.map(normalizeVerificationRecord);
|
|
1515
|
+
const gate = summarizeGate(checks);
|
|
1516
|
+
return {
|
|
1517
|
+
skill: skillName,
|
|
1518
|
+
...result,
|
|
1519
|
+
status: gate,
|
|
1520
|
+
effectiveStatus: gate,
|
|
1521
|
+
reason: gate === 'pass' ? 'OK' : checks.find((check: any) => check.effectiveStatus !== 'pass')?.reason ?? 'CHECK_FAILED',
|
|
1522
|
+
action: buildAction(gate, checks),
|
|
1523
|
+
checks
|
|
1514
1524
|
};
|
|
1515
1525
|
}
|
|
1516
1526
|
|
|
@@ -20,6 +20,13 @@ type VerificationCheckResult = {
|
|
|
20
20
|
message: string;
|
|
21
21
|
fail_type?: string;
|
|
22
22
|
warnings?: string[];
|
|
23
|
+
checkId?: string;
|
|
24
|
+
classification?: 'hard' | 'soft' | 'info';
|
|
25
|
+
effectiveStatus?: VerificationStatus;
|
|
26
|
+
reason?: string;
|
|
27
|
+
action?: string;
|
|
28
|
+
dependsOn?: string;
|
|
29
|
+
subchecks?: VerificationCheckResult[];
|
|
23
30
|
};
|
|
24
31
|
type VerificationContext = {
|
|
25
32
|
skillName: string;
|