@fitlab-ai/agent-infra 0.11.3 → 0.11.4
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 +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +41 -1
- package/dist/lib/internal/task-ledger.js +13 -5
- package/dist/lib/internal/task-operation-registry.js +1 -0
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/sandbox/control/server.js +26 -143
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +1 -26
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +1 -0
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +3 -2
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization.js +301 -143
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +10 -0
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-finalization.js +3 -16
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/verification-engine.js +20 -30
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workflow-warning-intents.js +21 -6
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +40 -1
- package/lib/internal/task-ledger.ts +12 -5
- package/lib/internal/task-operation-registry.ts +1 -0
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/sandbox/control/server.ts +21 -146
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +1 -26
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +1 -0
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +3 -1
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization.ts +269 -111
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +13 -0
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-finalization.ts +3 -16
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/verification-engine.ts +19 -30
- package/lib/task/verification.ts +2 -1
- package/lib/task/workflow-warning-intents.ts +18 -6
- package/lib/task/workspace-snapshot.ts +3 -2
- package/package.json +1 -1
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.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/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- package/lib/sandbox/control/finalization-response.ts +0 -35
|
@@ -15,8 +15,11 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
15
15
|
import { writeTask } from './write.ts';
|
|
16
16
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
17
17
|
import { allowsManualOverride } from './guard-override.ts';
|
|
18
|
-
import { parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
|
|
19
|
-
import type { ReworkIntent, ReworkTarget } from './rework-intent.ts';
|
|
18
|
+
import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
|
|
19
|
+
import type { ReworkClassification, ReworkIntent, ReworkTarget } from './rework-intent.ts';
|
|
20
|
+
import { extractSection, extractSubSection } from './sections.ts';
|
|
21
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
22
|
+
import { scanVisibleMarkdown } from './markdown.ts';
|
|
20
23
|
|
|
21
24
|
type ReviewSeverity = 'blocker' | 'major' | 'minor';
|
|
22
25
|
type ExecutorResponse = 'accepted' | 'adjusted' | 'refuted' | 'cannot-judge';
|
|
@@ -27,7 +30,8 @@ type LedgerIntent =
|
|
|
27
30
|
| { kind: 'finding-review'; taskRef: string; id: string; status: ReviewDisposition; evidence: string; needsImplementation?: boolean; dryRun?: boolean }
|
|
28
31
|
| { kind: 'decision-next-id'; taskRef: string }
|
|
29
32
|
| { kind: 'decision-upsert'; taskRef: string; id: string; stage: ReviewStage; artifact: string; needsImplementation?: boolean; dryRun?: boolean }
|
|
30
|
-
| { kind: 'rework-intent-upsert'; taskRef: string; intentId: string; findingId: string; sourceArtifact: string; sourceSha256: string;
|
|
33
|
+
| { kind: 'rework-intent-upsert'; taskRef: string; intentId: string; findingId: string; sourceArtifact: string; sourceSha256: string; classification: ReworkClassification; dryRun?: boolean }
|
|
34
|
+
| { kind: 'rework-intent-rebuild'; taskRef: string; findingId?: string; sourceArtifact?: string; sourceSha256?: string; classification?: ReworkClassification; dryRun?: boolean };
|
|
31
35
|
|
|
32
36
|
type LedgerIntentError = { code: string; message: string };
|
|
33
37
|
type LedgerIntentResult = {
|
|
@@ -39,6 +43,7 @@ type LedgerIntentResult = {
|
|
|
39
43
|
before: LedgerRow | null;
|
|
40
44
|
after: LedgerRow | null;
|
|
41
45
|
operations: readonly TaskOperationSummary[];
|
|
46
|
+
rebuild?: Readonly<{ mode: 'format-only' | 'pending-reclassification'; beforeSha256: string; afterSha256: string; converted: number; newIntentId: string | null }>;
|
|
42
47
|
error: LedgerIntentError | null;
|
|
43
48
|
};
|
|
44
49
|
|
|
@@ -60,12 +65,53 @@ function ledgerReadErrorCode(error: unknown): string {
|
|
|
60
65
|
return code === 'TABLE_DUPLICATE_KEY' ? 'LEDGER_DUPLICATE_ID' : 'LEDGER_DOCUMENT_INVALID';
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
const CLASSIFICATION_TARGET: Record<ReworkClassification, ReworkTarget> = {
|
|
69
|
+
'scope-or-requirement': 'analysis', design: 'plan', implementation: 'code',
|
|
70
|
+
'human-decision': 'pause', 'insufficient-evidence': 'pause'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function semanticDigest(value: string): string {
|
|
74
|
+
return createHash('sha256').update(value.normalize('NFKC').replace(/\s+/g, ' ').trim()).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizedTaskFact(value: string): string {
|
|
78
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function semanticFindingEvidence(reviewContent: string, heading: ReturnType<typeof scanVisibleMarkdown>['headings'][number], end: number): string {
|
|
82
|
+
const title = heading.text.replace(/^\d+[.、::]\s*/, '').trim();
|
|
83
|
+
return `${title}\n${reviewContent.slice(heading.end, end)}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findingEvidence(reviewContent: string, evidence: string): string | null {
|
|
87
|
+
const anchor = evidence.split('#')[1];
|
|
88
|
+
if (!anchor) return null;
|
|
89
|
+
const markdown = scanVisibleMarkdown(reviewContent);
|
|
90
|
+
const explicit = markdown.anchors.find((candidate) => candidate.id === anchor);
|
|
91
|
+
if (explicit) {
|
|
92
|
+
const heading = markdown.headings.find((candidate) => candidate.start > explicit.start);
|
|
93
|
+
const next = heading && markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
|
|
94
|
+
const end = next?.start ?? reviewContent.length;
|
|
95
|
+
return heading ? semanticFindingEvidence(reviewContent, heading, end) : reviewContent.slice(explicit.end, end);
|
|
96
|
+
}
|
|
97
|
+
const heading = markdown.headings.find((candidate) => {
|
|
98
|
+
const escapedAnchor = anchor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
return candidate.text === anchor || new RegExp(`^${escapedAnchor}(?:\\s|[.、::])`).test(candidate.text);
|
|
100
|
+
});
|
|
101
|
+
if (!heading) return null;
|
|
102
|
+
const next = markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
|
|
103
|
+
return semanticFindingEvidence(reviewContent, heading, next?.start ?? reviewContent.length);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function taskFactDigest(taskDir: string, content: string): string {
|
|
107
|
+
const analysisNames = fs.readdirSync(taskDir).filter((name) => /^analysis(?:-r\d+)?\.md$/.test(name));
|
|
108
|
+
const latest = analysisNames.map((name) => ({ name, round: parseArtifactName(name)?.round ?? 0 })).sort((a, b) => b.round - a.round)[0];
|
|
109
|
+
const flow = latest ? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, latest.name), 'utf8')) : null;
|
|
110
|
+
return semanticDigest(JSON.stringify({
|
|
111
|
+
taskInput: normalizedTaskFact(extractSection(content, ['任务输入', 'Task Input'])),
|
|
112
|
+
requirements: normalizedTaskFact(extractSection(content, ['需求', 'Requirements'])),
|
|
113
|
+
flow: flow?.status === 'valid' ? flow.decision.semanticDigest : flow?.status ?? 'missing'
|
|
114
|
+
}));
|
|
69
115
|
}
|
|
70
116
|
|
|
71
117
|
function nextFindingId(rows: readonly LedgerRow[], stage: ReviewStage): string {
|
|
@@ -123,11 +169,14 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
123
169
|
}
|
|
124
170
|
|
|
125
171
|
if (intent.kind === 'rework-intent-upsert') {
|
|
126
|
-
if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !
|
|
127
|
-
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity,
|
|
172
|
+
if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !(intent.classification in CLASSIFICATION_TARGET) || !/^[a-f0-9]{64}$/.test(intent.sourceSha256)) {
|
|
173
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, classification, or source hash is invalid', resolved.taskId, intent.intentId);
|
|
128
174
|
}
|
|
129
175
|
const finding = rows.find((row) => row.id === intent.findingId);
|
|
130
176
|
if (!finding || finding.severity === 'decision') return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
|
|
177
|
+
if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
|
|
178
|
+
return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId, intent.findingId);
|
|
179
|
+
}
|
|
131
180
|
const sourceFromEvidence = finding.evidence.split('#')[0];
|
|
132
181
|
if (sourceFromEvidence !== intent.sourceArtifact || parseArtifactName(intent.sourceArtifact)?.family !== `review-${finding.stage}`) {
|
|
133
182
|
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
|
|
@@ -137,12 +186,34 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
137
186
|
if (actualHash !== intent.sourceSha256) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact hash does not match', resolved.taskId, intent.intentId);
|
|
138
187
|
const parsed = parseReworkIntentDocument(content);
|
|
139
188
|
if (!parsed.ok) return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
|
|
140
|
-
const
|
|
189
|
+
const reviewContent = fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact), 'utf8');
|
|
190
|
+
const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
|
|
191
|
+
if (!evidenceBlock) return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId, intent.intentId);
|
|
192
|
+
const evidenceDigest = semanticDigest(evidenceBlock);
|
|
193
|
+
const factsDigest = taskFactDigest(resolved.taskDir, content);
|
|
194
|
+
const requestedIntent: ReworkIntent = {
|
|
141
195
|
intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
|
|
142
|
-
sourceSha256: intent.sourceSha256, target: intent.
|
|
196
|
+
sourceSha256: intent.sourceSha256, target: CLASSIFICATION_TARGET[intent.classification], classification: intent.classification,
|
|
197
|
+
evidenceDigest, taskFactDigest: factsDigest, status: 'pending',
|
|
143
198
|
declaredAt: new Date().toISOString(), consumedAt: ''
|
|
144
199
|
};
|
|
145
|
-
|
|
200
|
+
if (parsed.intents.some((candidate) => candidate.intentId === intent.intentId)) {
|
|
201
|
+
const replay = upsertReworkIntent(parsed.intents, requestedIntent);
|
|
202
|
+
if (!replay.changed) {
|
|
203
|
+
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const same = parsed.intents.find((candidate) => candidate.findingId === intent.findingId
|
|
207
|
+
&& candidate.evidenceDigest === evidenceDigest && candidate.taskFactDigest === factsDigest
|
|
208
|
+
&& candidate.classification === intent.classification && candidate.target === CLASSIFICATION_TARGET[intent.classification]);
|
|
209
|
+
const classification: ReworkClassification = same ? 'insufficient-evidence' : intent.classification;
|
|
210
|
+
const nextIntent: ReworkIntent = {
|
|
211
|
+
...requestedIntent, target: CLASSIFICATION_TARGET[classification], classification
|
|
212
|
+
};
|
|
213
|
+
const now = nextIntent.declaredAt;
|
|
214
|
+
const superseded = parsed.intents.map((candidate) => candidate.status === 'pending'
|
|
215
|
+
? { ...candidate, status: 'superseded' as const, consumedAt: now } : candidate);
|
|
216
|
+
const next = upsertReworkIntent(superseded, nextIntent);
|
|
146
217
|
if (!next.changed) return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
147
218
|
const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next.intents)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
148
219
|
if (result.status === 'failed') return failed(intent, result.error.code, result.error.message, result.taskId, intent.intentId);
|
|
@@ -152,6 +223,75 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
152
223
|
}
|
|
153
224
|
}
|
|
154
225
|
|
|
226
|
+
if (intent.kind === 'rework-intent-rebuild') {
|
|
227
|
+
const beforeSha256 = createHash('sha256').update(content).digest('hex');
|
|
228
|
+
const supplied = [intent.findingId, intent.sourceArtifact, intent.sourceSha256, intent.classification].filter((value) => value !== undefined).length;
|
|
229
|
+
if (supplied !== 0 && supplied !== 4) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild binding options must be provided together', resolved.taskId);
|
|
230
|
+
const current = parseReworkIntentDocument(content);
|
|
231
|
+
if (current.ok && current.present) {
|
|
232
|
+
if (supplied !== 0) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'current rework intent format does not accept rebuild binding options', resolved.taskId);
|
|
233
|
+
return {
|
|
234
|
+
status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: null,
|
|
235
|
+
before: null, after: null, operations: [], error: null,
|
|
236
|
+
rebuild: { mode: 'format-only', beforeSha256, afterSha256: beforeSha256, converted: current.intents.length, newIntentId: null }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const legacy = parseLegacyReworkIntentDocument(content);
|
|
240
|
+
if (!legacy.ok) return failed(intent, legacy.code, legacy.message, resolved.taskId);
|
|
241
|
+
const pending = legacy.intents.filter((row) => row.status === 'pending');
|
|
242
|
+
if (pending.length === 0 && supplied !== 0) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'format-only rebuild does not accept finding or classification options', resolved.taskId);
|
|
243
|
+
if (pending.length > 0 && supplied !== 4) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'legacy pending intents require finding, review, hash, and classification', resolved.taskId);
|
|
244
|
+
const now = new Date().toISOString();
|
|
245
|
+
let next = legacy.intents.map((row) => row.status === 'pending' ? { ...row, status: 'superseded' as const, consumedAt: now } : row);
|
|
246
|
+
let entityId: string | null = null;
|
|
247
|
+
if (pending.length > 0) {
|
|
248
|
+
const finding = rows.find((row) => row.id === intent.findingId);
|
|
249
|
+
if (!finding || finding.severity === 'decision' || finding.evidence.split('#')[0] !== intent.sourceArtifact) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild binding does not match a review finding', resolved.taskId);
|
|
250
|
+
if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
|
|
251
|
+
return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId);
|
|
252
|
+
}
|
|
253
|
+
const sourceIdentity = parseArtifactName(intent.sourceArtifact!);
|
|
254
|
+
const expectedFamily = `review-${finding.stage}`;
|
|
255
|
+
const latestSource = fs.readdirSync(resolved.taskDir)
|
|
256
|
+
.map((name) => parseArtifactName(name))
|
|
257
|
+
.filter((identity) => identity?.family === expectedFamily)
|
|
258
|
+
.sort((left, right) => right!.round - left!.round || left!.name.localeCompare(right!.name))[0];
|
|
259
|
+
if (!sourceIdentity || sourceIdentity.family !== expectedFamily || latestSource?.name !== sourceIdentity.name) {
|
|
260
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact is not the latest review for the finding stage', resolved.taskId);
|
|
261
|
+
}
|
|
262
|
+
const sourcePath = path.join(resolved.taskDir, intent.sourceArtifact!);
|
|
263
|
+
const actualHash = createHash('sha256').update(fs.readFileSync(sourcePath)).digest('hex');
|
|
264
|
+
if (actualHash !== intent.sourceSha256) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact hash does not match', resolved.taskId);
|
|
265
|
+
const max = next.reduce((value, row) => Math.max(value, Number(row.intentId.slice(3))), 0);
|
|
266
|
+
entityId = `RI-${max + 1}`;
|
|
267
|
+
const reviewContent = fs.readFileSync(sourcePath, 'utf8');
|
|
268
|
+
const classification = intent.classification!;
|
|
269
|
+
if (!(classification in CLASSIFICATION_TARGET)) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild classification is invalid', resolved.taskId);
|
|
270
|
+
const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
|
|
271
|
+
if (!evidenceBlock) return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId);
|
|
272
|
+
next = [...next, {
|
|
273
|
+
intentId: entityId, findingId: intent.findingId!, sourceArtifact: intent.sourceArtifact!, sourceSha256: intent.sourceSha256!,
|
|
274
|
+
target: CLASSIFICATION_TARGET[classification], classification,
|
|
275
|
+
evidenceDigest: semanticDigest(evidenceBlock),
|
|
276
|
+
taskFactDigest: taskFactDigest(resolved.taskDir, content), status: 'pending', declaredAt: now, consumedAt: ''
|
|
277
|
+
}];
|
|
278
|
+
}
|
|
279
|
+
const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
280
|
+
if (result.status === 'failed') return failed(intent, result.error.code, result.error.message, result.taskId, entityId);
|
|
281
|
+
const afterContent = result.status === 'applied' || result.status === 'no-op' ? fs.readFileSync(resolved.taskMdPath, 'utf8') : content;
|
|
282
|
+
return {
|
|
283
|
+
status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId,
|
|
284
|
+
before: null, after: null, operations: result.operations, error: null,
|
|
285
|
+
rebuild: {
|
|
286
|
+
mode: pending.length > 0 ? 'pending-reclassification' : 'format-only',
|
|
287
|
+
beforeSha256,
|
|
288
|
+
afterSha256: createHash('sha256').update(afterContent).digest('hex'),
|
|
289
|
+
converted: legacy.intents.length,
|
|
290
|
+
newIntentId: entityId
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
155
295
|
let before: LedgerRow | null = null;
|
|
156
296
|
let after: LedgerRow;
|
|
157
297
|
if (intent.kind === 'finding-upsert') {
|
|
@@ -213,7 +353,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
213
353
|
? new Set(['confirmed', 'open', 'needs-human-decision'])
|
|
214
354
|
: before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set<string>();
|
|
215
355
|
const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
|
|
216
|
-
if ((
|
|
356
|
+
if ((!sameRoundMinorClose && !allowed.has(intent.status))
|
|
217
357
|
&& !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
|
|
218
358
|
return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
|
|
219
359
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { extractSection } from './sections.ts';
|
|
4
|
+
|
|
5
|
+
const PATH_STAGES = {
|
|
6
|
+
streamlined: ['analysis', 'code', 'review-code'],
|
|
7
|
+
standard: ['analysis', 'plan', 'code', 'review-code'],
|
|
8
|
+
full: ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
type LifecyclePath = keyof typeof PATH_STAGES;
|
|
12
|
+
type LifecycleStage = (typeof PATH_STAGES)[LifecyclePath][number];
|
|
13
|
+
type LifecyclePathDecision = Readonly<{
|
|
14
|
+
path: LifecyclePath;
|
|
15
|
+
stages: readonly LifecycleStage[];
|
|
16
|
+
basis: string;
|
|
17
|
+
unmetHigherConditions: string;
|
|
18
|
+
upgradeTriggers: string;
|
|
19
|
+
semanticDigest: string;
|
|
20
|
+
sourceArtifact: string;
|
|
21
|
+
sourceSha256: string;
|
|
22
|
+
}>;
|
|
23
|
+
type LifecyclePathState =
|
|
24
|
+
| Readonly<{ status: 'missing'; decision: null; message: string }>
|
|
25
|
+
| Readonly<{ status: 'invalid'; decision: null; message: string }>
|
|
26
|
+
| Readonly<{ status: 'valid'; decision: LifecyclePathDecision; message: null }>;
|
|
27
|
+
|
|
28
|
+
const FIELD_ALIASES = {
|
|
29
|
+
path: ['本任务路径', 'Path'],
|
|
30
|
+
basis: ['判定依据', 'Basis'],
|
|
31
|
+
unmetHigherConditions: ['未满足的更高路径条件', '未满足的较低路径条件', 'Unmet Higher-path Conditions'],
|
|
32
|
+
upgradeTriggers: ['升级触发条件', 'Upgrade Triggers']
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
function normalizeText(value: string): string {
|
|
36
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim().replace(/[。.]$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function digest(value: unknown): string {
|
|
40
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pathToken(value: string): LifecyclePath | null {
|
|
44
|
+
const normalized = normalizeText(value).toLowerCase().replace(/[`*_]/g, '');
|
|
45
|
+
if (/^(?:streamlined|精简路径|精简)$/.test(normalized)) return 'streamlined';
|
|
46
|
+
if (/^(?:standard|标准路径|标准)$/.test(normalized)) return 'standard';
|
|
47
|
+
if (/^(?:full|完整路径|完整)$/.test(normalized)) return 'full';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fieldValues(body: string, aliases: readonly string[]): string[] {
|
|
52
|
+
const wanted = new Set(aliases);
|
|
53
|
+
const values: string[] = [];
|
|
54
|
+
for (const line of body.split(/\r?\n/)) {
|
|
55
|
+
const match = /^\s*[-*]\s+\*\*([^*]+)\*\*[::]\s*(.*?)\s*$/.exec(line);
|
|
56
|
+
if (match && wanted.has(match[1]!.trim())) values.push(match[2]!.trim());
|
|
57
|
+
}
|
|
58
|
+
return values;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseLifecyclePathDecision(
|
|
62
|
+
analysisContent: string,
|
|
63
|
+
sourceArtifact = '',
|
|
64
|
+
sourceSha256 = ''
|
|
65
|
+
): LifecyclePathState {
|
|
66
|
+
const body = extractSection(analysisContent, ['流程裁定', 'Flow Decision']);
|
|
67
|
+
if (!body) return { status: 'missing', decision: null, message: 'analysis flow decision section is missing' };
|
|
68
|
+
const fields = Object.fromEntries(Object.entries(FIELD_ALIASES).map(([key, aliases]) => [key, fieldValues(body, aliases)])) as Record<keyof typeof FIELD_ALIASES, string[]>;
|
|
69
|
+
const invalid = Object.entries(fields).find(([, values]) => values.length !== 1);
|
|
70
|
+
if (invalid) return { status: 'invalid', decision: null, message: `flow decision field '${invalid[0]}' must appear exactly once` };
|
|
71
|
+
const path = pathToken(fields.path[0]!);
|
|
72
|
+
if (!path) return { status: 'invalid', decision: null, message: `unknown lifecycle path '${fields.path[0]}'` };
|
|
73
|
+
const basis = normalizeText(fields.basis[0]!);
|
|
74
|
+
const unmetHigherConditions = normalizeText(fields.unmetHigherConditions[0]!);
|
|
75
|
+
const upgradeTriggers = normalizeText(fields.upgradeTriggers[0]!);
|
|
76
|
+
if (!basis || !unmetHigherConditions || !upgradeTriggers) return { status: 'invalid', decision: null, message: 'flow decision fields must be non-empty' };
|
|
77
|
+
return {
|
|
78
|
+
status: 'valid',
|
|
79
|
+
message: null,
|
|
80
|
+
decision: {
|
|
81
|
+
path,
|
|
82
|
+
stages: PATH_STAGES[path],
|
|
83
|
+
basis,
|
|
84
|
+
unmetHigherConditions,
|
|
85
|
+
upgradeTriggers,
|
|
86
|
+
semanticDigest: digest({ path, basis, unmetHigherConditions, upgradeTriggers }),
|
|
87
|
+
sourceArtifact,
|
|
88
|
+
sourceSha256
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function pathIncludes(state: LifecyclePathState | undefined, stage: LifecycleStage): boolean {
|
|
94
|
+
return state?.status === 'valid' && state.decision.stages.includes(stage);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export { PATH_STAGES, parseLifecyclePathDecision, pathIncludes };
|
|
98
|
+
export type { LifecyclePath, LifecyclePathDecision, LifecyclePathState, LifecycleStage };
|
package/lib/task/lifecycle.ts
CHANGED
|
@@ -108,6 +108,9 @@ type TaskLifecycleOptions = {
|
|
|
108
108
|
taskFileSystem?: Partial<TaskFileSystem>;
|
|
109
109
|
directoryRenameSync?: (source: string, target: string) => void;
|
|
110
110
|
manualOverride?: Readonly<{ failureId: string; operator: string; reason: string }>;
|
|
111
|
+
/** Write the terminal task document and durable journal, but leave the move
|
|
112
|
+
* and short-id mutation for a separately authorized commit. */
|
|
113
|
+
prepareOnly?: boolean;
|
|
111
114
|
};
|
|
112
115
|
|
|
113
116
|
function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
|
|
@@ -574,6 +577,16 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
574
577
|
try { writeJournal(journalPath, journal, io); }
|
|
575
578
|
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
576
579
|
}
|
|
580
|
+
if (options.prepareOnly && !completed.has('directory-moved')) {
|
|
581
|
+
return {
|
|
582
|
+
...failed(request, { code: '', message: '' }), status: 'applied', changed: true, error: null,
|
|
583
|
+
taskId, sourceState, targetState: spec.target, sourcePath, targetPath,
|
|
584
|
+
task: { operations: taskOperations }, directory: { effect: 'unchanged', changed: false },
|
|
585
|
+
shortId: { effect: 'unchanged', shortId: null, changed: false },
|
|
586
|
+
timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion,
|
|
587
|
+
journalPath, completedSteps: [...completed], pendingSteps: STEPS.filter((step) => !completed.has(step))
|
|
588
|
+
};
|
|
589
|
+
}
|
|
577
590
|
if (fs.existsSync(targetPath) && !fs.existsSync(sourcePath)) completed.add('directory-moved');
|
|
578
591
|
if (!completed.has('directory-moved')) {
|
|
579
592
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
@@ -4,11 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { inspectDecisionDetailDuplicates } from './decision-details.ts';
|
|
5
5
|
import { scanVisibleMarkdown } from './markdown.ts';
|
|
6
6
|
import { parseArtifactName } from './artifact-name.ts';
|
|
7
|
-
import {
|
|
8
|
-
hasOpenArtifactRound,
|
|
9
|
-
} from './artifact-lifecycle.ts';
|
|
10
7
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
11
|
-
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
12
8
|
import { canonicalSemanticDigest, inspectArtifactPatterns, inspectArtifactStructure, sha256Content } from './artifact-operations.ts';
|
|
13
9
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
14
10
|
|
|
@@ -28,8 +24,7 @@ type LocalArtifactDiagnosticCode =
|
|
|
28
24
|
| 'LOCAL_DECISION_DETAIL_DUPLICATE'
|
|
29
25
|
| 'LOCAL_REQUIRED_PATTERN_MISSING'
|
|
30
26
|
| 'LOCAL_STRUCTURAL_INVALID'
|
|
31
|
-
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION'
|
|
32
|
-
| 'LOCAL_QUALIFICATION_AUDIT_INVALID';
|
|
27
|
+
| 'LOCAL_SECTION_HEADING_TRAILING_PUNCTUATION';
|
|
33
28
|
|
|
34
29
|
type LocalArtifactDiagnostic = {
|
|
35
30
|
code: LocalArtifactDiagnosticCode;
|
|
@@ -40,8 +35,6 @@ type LocalArtifactDiagnostic = {
|
|
|
40
35
|
type LocalArtifactValidationOptions = {
|
|
41
36
|
family: LocalArtifactFamily;
|
|
42
37
|
requiredSections?: readonly string[];
|
|
43
|
-
taskContent?: string;
|
|
44
|
-
artifact?: string;
|
|
45
38
|
};
|
|
46
39
|
|
|
47
40
|
type LocalArtifactValidationResult = {
|
|
@@ -128,20 +121,6 @@ function validateLocalArtifact(
|
|
|
128
121
|
const decisionDetails = inspectDecisionDetailDuplicates(content);
|
|
129
122
|
if (!decisionDetails.ok) diagnostics.push({ code: 'LOCAL_DECISION_DETAIL_DUPLICATE', message: decisionDetails.message, line: null });
|
|
130
123
|
|
|
131
|
-
if (options.taskContent !== undefined) {
|
|
132
|
-
const identity = options.artifact ? parseArtifactName(options.artifact) : null;
|
|
133
|
-
const expected = identity?.family === options.family && hasOpenArtifactRound(options.taskContent, options.family, identity.round)
|
|
134
|
-
? expectedQualificationRelations(options.taskContent, options.family)
|
|
135
|
-
: undefined;
|
|
136
|
-
if (expected && !expected.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${expected.code}: ${expected.message}`, line: null });
|
|
137
|
-
const qualification = validateQualificationAudit(options.taskContent, content, {
|
|
138
|
-
family: options.family,
|
|
139
|
-
artifact: options.artifact,
|
|
140
|
-
expectedUpstreamRelations: expected?.ok ? expected.relations : undefined
|
|
141
|
-
});
|
|
142
|
-
if (!qualification.ok) diagnostics.push({ code: 'LOCAL_QUALIFICATION_AUDIT_INVALID', message: `${qualification.code}: ${qualification.message}`, line: null });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
124
|
return {
|
|
146
125
|
ok: diagnostics.length === 0,
|
|
147
126
|
family: options.family,
|
|
@@ -182,10 +161,8 @@ function prepareLocalArtifact(
|
|
|
182
161
|
if (!resolved.ok) return failed(resolved.code, resolved.message);
|
|
183
162
|
const parsed = parseArtifactName(request.artifact);
|
|
184
163
|
if (!parsed || parsed.family !== request.family) return failed('ARTIFACT_IDENTITY_INVALID', `artifact '${request.artifact}' does not match ${request.family}`);
|
|
185
|
-
let taskContent: string;
|
|
186
164
|
let content: string;
|
|
187
165
|
try {
|
|
188
|
-
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
189
166
|
const artifactPath = path.join(resolved.taskDir, request.artifact);
|
|
190
167
|
if (!fs.statSync(artifactPath).isFile()) return failed('ARTIFACT_NOT_REGULAR', `artifact '${request.artifact}' is not a regular file`);
|
|
191
168
|
content = candidate ?? fs.readFileSync(artifactPath, 'utf8');
|
|
@@ -195,7 +172,7 @@ function prepareLocalArtifact(
|
|
|
195
172
|
return failed('ARTIFACT_IDENTITY_INVALID', 'artifact finalization requires an active task');
|
|
196
173
|
}
|
|
197
174
|
const validation = validateLocalArtifact(content, {
|
|
198
|
-
family: request.family, requiredSections: request.requiredSections
|
|
175
|
+
family: request.family, requiredSections: request.requiredSections
|
|
199
176
|
});
|
|
200
177
|
const artifactSha256 = sha256Content(content);
|
|
201
178
|
const result = failedFinalization(request, {
|
|
@@ -59,6 +59,7 @@ import { hasOpenLifecycleExecution } from './activity-log.ts';
|
|
|
59
59
|
import { reconcileTaskInvalidation } from './invalidation-command.ts';
|
|
60
60
|
import type { LifecycleAction, LifecycleFacts } from './capabilities.ts';
|
|
61
61
|
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
62
|
+
import { resolveArtifactContext } from './artifact-lifecycle.ts';
|
|
62
63
|
|
|
63
64
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
64
65
|
type ModelPolicySource = Readonly<{
|
|
@@ -642,18 +643,14 @@ function validateSavedCurrentDiffBase(
|
|
|
642
643
|
return null;
|
|
643
644
|
}
|
|
644
645
|
|
|
645
|
-
function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort'> | { completion: true } | null {
|
|
646
|
+
function routeFromFacts(facts: LifecycleFacts): Omit<OrchestrationNext, 'requestedModel' | 'requestedReasoningEffort' | 'round' | 'artifact'> | { completion: true } | null {
|
|
646
647
|
const recommendation = recommendNext(facts);
|
|
647
|
-
if (!recommendation.action) return { completion: true };
|
|
648
|
+
if (!recommendation.action) return recommendation.reasonCode === 'LIFECYCLE_REVIEWED' ? { completion: true } : null;
|
|
648
649
|
const action = recommendation.action;
|
|
649
650
|
if (action === 'manual-validation' || action === 'validation-run') return null;
|
|
650
|
-
const family = action === 'analysis' ? 'analysis' : action;
|
|
651
|
-
const round = maxArtifactRound([
|
|
652
|
-
...(facts.artifacts[family] ?? []), ...(facts.staleArtifacts?.[family] ?? [])
|
|
653
|
-
], family) + 1;
|
|
654
651
|
const command = action === 'analysis' ? 'analyze-task' : action === 'plan' ? 'plan-task' : action === 'code' ? 'code-task' : action;
|
|
655
652
|
const role = action.startsWith('review-') ? 'reviewer' : 'executor';
|
|
656
|
-
return { action: command, role, stage: action
|
|
653
|
+
return { action: command, role, stage: action };
|
|
657
654
|
}
|
|
658
655
|
|
|
659
656
|
function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
@@ -786,14 +783,35 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
786
783
|
return { status: 'completed', changed: true, taskId: resolved.taskId, run: completed, next: null, error: null };
|
|
787
784
|
}
|
|
788
785
|
const action = routed.stage as LifecycleAction;
|
|
786
|
+
const family = action === 'analysis' ? 'analysis' : action;
|
|
787
|
+
const artifactContext = resolveArtifactContext(taskRef, family, {
|
|
788
|
+
repoRoot: resolved.repoRoot
|
|
789
|
+
});
|
|
790
|
+
if (artifactContext.status !== 'ready' || !artifactContext.selection) {
|
|
791
|
+
return failed(
|
|
792
|
+
artifactContext.error?.code ?? 'ORCHESTRATION_ARTIFACT_SELECTION_FAILED',
|
|
793
|
+
artifactContext.error?.message ?? 'cannot determine artifact selection',
|
|
794
|
+
resolved.taskId
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (artifactContext.selection.disposition === 'reuse') {
|
|
798
|
+
return failed(
|
|
799
|
+
'ORCHESTRATION_REUSE_STALLED',
|
|
800
|
+
`lifecycle still routes to ${action} after reusing ${artifactContext.selection.artifact.name}`,
|
|
801
|
+
resolved.taskId
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
const selected = artifactContext.selection.artifact;
|
|
789
805
|
const capability = canStart(action, facts.facts, {
|
|
790
|
-
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${
|
|
806
|
+
initiator: 'orchestrator', requestId: `orchestration:${resolved.taskId}:${routed.stage}:${selected.round}`,
|
|
791
807
|
requestedAction: action, reasonCode: 'user-request', explicitRequest: true
|
|
792
808
|
});
|
|
793
809
|
if (!capability.allowed) return failed('ORCHESTRATION_CAPABILITY_DENIED', `${capability.reasonCode}: ${capability.evidence.join(', ')}`, resolved.taskId);
|
|
794
810
|
const policy = run ? rolePolicy(run, routed.role) : null;
|
|
795
811
|
const next: OrchestrationNext = {
|
|
796
812
|
...routed,
|
|
813
|
+
round: selected.round,
|
|
814
|
+
artifact: selected.name,
|
|
797
815
|
requestedModel: policy?.model ?? null,
|
|
798
816
|
requestedReasoningEffort: policy?.reasoningEffort ?? null
|
|
799
817
|
};
|
|
@@ -16,7 +16,6 @@ import { allowsManualOverride } from './guard-override.ts';
|
|
|
16
16
|
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
17
17
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
18
18
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
19
|
-
import { expectedQualificationRelations, validateQualificationAudit } from './qualification-audit.ts';
|
|
20
19
|
|
|
21
20
|
type ReviewFinalizationErrorCode =
|
|
22
21
|
| ResolveTaskRefErrorCode
|
|
@@ -30,7 +29,6 @@ type ReviewFinalizationErrorCode =
|
|
|
30
29
|
| 'REVIEW_SUMMARY_PLACEHOLDER_INVALID'
|
|
31
30
|
| 'REVIEW_SUMMARY_COUNT_MISMATCH'
|
|
32
31
|
| 'REVIEW_ARTIFACT_STRUCTURE_INVALID'
|
|
33
|
-
| 'REVIEW_ARTIFACT_QUALIFICATION_INVALID'
|
|
34
32
|
| 'REVIEW_DECISION_DETAIL_INVALID'
|
|
35
33
|
| 'REVIEW_ARTIFACT_CONFLICT'
|
|
36
34
|
| 'REVIEW_PROVENANCE_INVALID'
|
|
@@ -143,10 +141,8 @@ function preflightReviewSummaryUnlocked(
|
|
|
143
141
|
const publicationError = validateArtifactPublication(resolved.taskDir, spec.family, request.artifact);
|
|
144
142
|
if (publicationError) return preflightFailed(request, 'REVIEW_ARTIFACT_IDENTITY_INVALID', publicationError.message, resolved.taskId);
|
|
145
143
|
|
|
146
|
-
let taskContent: string;
|
|
147
144
|
let content: string;
|
|
148
145
|
try {
|
|
149
|
-
taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
150
146
|
const validated = validateCompletedArtifact(resolved.taskDir, spec.family, request.artifact);
|
|
151
147
|
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);
|
|
152
148
|
content = fs.readFileSync(validated.artifact.path, 'utf8');
|
|
@@ -163,21 +159,12 @@ function preflightReviewSummaryUnlocked(
|
|
|
163
159
|
const semanticDigest = canonicalSemanticDigest(content);
|
|
164
160
|
const detail = inspectDecisionDetailDuplicates(content);
|
|
165
161
|
const structure = inspectArtifactContract(content, getArtifactSchema(spec.family)!);
|
|
166
|
-
|
|
167
|
-
if (!expected.ok) return preflightFailed(request, 'REVIEW_ARTIFACT_QUALIFICATION_INVALID', `${expected.code}: ${expected.message}`, resolved.taskId, { artifactSha256, semanticDigest });
|
|
168
|
-
const qualification = validateQualificationAudit(taskContent, content, {
|
|
169
|
-
family: spec.family,
|
|
170
|
-
artifact: request.artifact,
|
|
171
|
-
expectedUpstreamRelations: expected.relations
|
|
172
|
-
});
|
|
173
|
-
const qualificationError = qualification.ok ? null : qualification;
|
|
174
|
-
if (!detail.ok || !structure.ok || qualificationError) {
|
|
162
|
+
if (!detail.ok || !structure.ok) {
|
|
175
163
|
const diagnostics = [
|
|
176
164
|
...(!detail.ok ? [`${detail.code}: ${detail.message}`] : []),
|
|
177
|
-
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`)
|
|
178
|
-
...(qualificationError ? [`${qualificationError.code}: ${qualificationError.message}`] : [])
|
|
165
|
+
...structure.diagnostics.map((item) => `${item.code}: ${item.message}`)
|
|
179
166
|
].join('; ');
|
|
180
|
-
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' :
|
|
167
|
+
return preflightFailed(request, !detail.ok ? 'REVIEW_DECISION_DETAIL_INVALID' : 'REVIEW_ARTIFACT_STRUCTURE_INVALID', diagnostics, resolved.taskId, {
|
|
181
168
|
artifactSha256, semanticDigest,
|
|
182
169
|
});
|
|
183
170
|
}
|