@fitlab-ai/agent-infra 0.11.3 → 0.11.5
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 -6
- package/dist/lib/decide.js +4 -41
- package/dist/lib/git/branch-worktree.js +45 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -4
- package/dist/lib/internal/task-activity.js +7 -40
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-event.js +4 -37
- package/dist/lib/internal/task-finalization.js +48 -1
- package/dist/lib/internal/task-ledger.js +14 -44
- package/dist/lib/internal/task-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +2 -4
- package/dist/lib/internal/task-review.js +5 -37
- package/dist/lib/internal/task-verify.js +3 -47
- package/dist/lib/internal/task-warning.js +4 -40
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/pr-summary.js +4 -23
- package/dist/lib/platform/verification-sync.js +12 -49
- package/dist/lib/sandbox/control/client.js +4 -1
- package/dist/lib/sandbox/control/executor.js +12 -4
- package/dist/lib/sandbox/control/protocol.js +2 -1
- package/dist/lib/sandbox/control/server.js +26 -143
- package/dist/lib/sandbox/removal.js +15 -8
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/activity-intent.js +1 -2
- 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 +37 -28
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +8 -7
- 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 +12 -168
- package/dist/lib/task/decision-intents.js +14 -4
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +68 -73
- package/dist/lib/task/finalization-handoff.js +92 -0
- package/dist/lib/task/finalization.js +423 -154
- package/dist/lib/task/intermediate-cleanup.js +9 -148
- package/dist/lib/task/ledger-intents.js +159 -20
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +60 -11
- package/dist/lib/task/local-artifact-finalization.js +13 -47
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-command.js +1 -2
- package/dist/lib/task/review-finalization.js +4 -18
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/snapshot.js +6 -6
- package/dist/lib/task/verification-engine.js +25 -30
- package/dist/lib/task/verification.js +2 -13
- package/dist/lib/task/workflow-warning-intents.js +23 -10
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lib/task/write.js +1 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/decide.ts +5 -35
- package/lib/git/branch-worktree.ts +41 -0
- package/lib/internal/cli-route-inventory.ts +1 -3
- package/lib/internal/task-activity.ts +6 -34
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-event.ts +4 -37
- package/lib/internal/task-finalization.ts +47 -1
- package/lib/internal/task-ledger.ts +15 -41
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +2 -4
- package/lib/internal/task-review.ts +5 -31
- package/lib/internal/task-verify.ts +3 -39
- package/lib/internal/task-warning.ts +3 -32
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/pr-summary.ts +4 -25
- package/lib/platform/verification-sync.ts +13 -53
- package/lib/sandbox/control/client.ts +5 -1
- package/lib/sandbox/control/executor.ts +13 -4
- package/lib/sandbox/control/protocol.ts +3 -2
- package/lib/sandbox/control/server.ts +21 -146
- package/lib/sandbox/removal.ts +23 -7
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/activity-intent.ts +1 -2
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +59 -28
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +10 -7
- 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 +17 -189
- package/lib/task/decision-intents.ts +11 -4
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +73 -74
- package/lib/task/finalization-handoff.ts +104 -0
- package/lib/task/finalization.ts +388 -122
- package/lib/task/intermediate-cleanup.ts +9 -125
- package/lib/task/ledger-intents.ts +157 -20
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +58 -13
- package/lib/task/local-artifact-finalization.ts +14 -56
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-command.ts +2 -5
- package/lib/task/review-finalization.ts +4 -20
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/snapshot.ts +6 -6
- package/lib/task/verification-engine.ts +29 -30
- package/lib/task/verification.ts +3 -16
- package/lib/task/workflow-warning-intents.ts +20 -10
- package/lib/task/workspace-snapshot.ts +3 -2
- package/lib/task/write.ts +1 -4
- 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 +21 -5
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
- 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 +7 -7
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
- 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/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-analysis/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
- 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-code/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
- 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/review-plan/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- 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/internal/task-override.js +0 -138
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- package/dist/lib/task/artifact-recovery.js +0 -476
- package/dist/lib/task/artifact-repair-intent.js +0 -181
- package/dist/lib/task/guard-override.js +0 -31
- package/dist/lib/task/human-override.js +0 -1259
- package/lib/internal/task-override.ts +0 -144
- package/lib/sandbox/control/finalization-response.ts +0 -35
- package/lib/task/artifact-recovery.ts +0 -572
- package/lib/task/artifact-repair-intent.ts +0 -227
- package/lib/task/guard-override.ts +0 -50
- package/lib/task/human-override.ts +0 -1522
|
@@ -14,26 +14,14 @@ import {
|
|
|
14
14
|
readTaskFinalizationReceipt,
|
|
15
15
|
type TaskFinalizationReceipt
|
|
16
16
|
} from './finalization.ts';
|
|
17
|
-
import {
|
|
18
|
-
semanticDigest,
|
|
19
|
-
sha256Content
|
|
20
|
-
} from './local-artifact-finalization.ts';
|
|
21
|
-
import { artifactRecoveryRoot, readArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
22
|
-
import type { ArtifactRecoveryIntent } from './artifact-repair-intent.ts';
|
|
23
|
-
import { parseArtifactName } from './artifact-name.ts';
|
|
24
|
-
import type { ArtifactSchemaFamily } from './artifact-schema.ts';
|
|
25
17
|
import { enumerateAllTaskDirs, type TaskWorkspaceState } from './resolve-ref.ts';
|
|
26
18
|
import type { TaskControlBindingEvidence } from './finalization-state.ts';
|
|
27
19
|
|
|
28
20
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
29
|
-
const LOCAL_INTENT_RE = /^(TASK-\d{8}-\d{6})-(analysis|review-analysis|plan|review-plan|code|review-code)-(.+\.md)\.json$/;
|
|
30
21
|
const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
|
|
31
|
-
const AUXILIARY_ROOTS = [
|
|
32
|
-
'.local-artifact-finalization-intents',
|
|
33
|
-
'.task-commit-intents'
|
|
34
|
-
] as const;
|
|
22
|
+
const AUXILIARY_ROOTS = ['.task-commit-intents'] as const;
|
|
35
23
|
|
|
36
|
-
type CleanupKind = '
|
|
24
|
+
type CleanupKind = 'COMMIT-SYNCED' | 'EMPTY-AUX-PARENT';
|
|
37
25
|
type CleanupDisposition = 'planned' | 'protected' | 'skipped' | 'deleted' | 'failed';
|
|
38
26
|
type FileIdentity = Readonly<{ dev: string; ino: string; size: number; mtimeMs: number }>;
|
|
39
27
|
|
|
@@ -153,47 +141,6 @@ function receiptGate(
|
|
|
153
141
|
return null;
|
|
154
142
|
}
|
|
155
143
|
|
|
156
|
-
function canonicalLocalIntentPath(taskId: string, family: string, artifact: string): string {
|
|
157
|
-
return `${taskId}-${family}-${artifact}.json`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function localIntentCandidate(
|
|
161
|
-
repoRoot: string,
|
|
162
|
-
task: TaskRecord | undefined,
|
|
163
|
-
filePath: string,
|
|
164
|
-
taskId: string,
|
|
165
|
-
family: ArtifactSchemaFamily,
|
|
166
|
-
artifact: string,
|
|
167
|
-
identity: FileIdentity,
|
|
168
|
-
options: ScanOptions
|
|
169
|
-
): IntermediateCleanupItem {
|
|
170
|
-
const protectedReason = taskGate(task);
|
|
171
|
-
if (protectedReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', protectedReason, identity);
|
|
172
|
-
if (path.basename(filePath) !== canonicalLocalIntentPath(taskId, family, artifact)
|
|
173
|
-
|| !parseArtifactName(artifact)?.family || parseArtifactName(artifact)!.family !== family) {
|
|
174
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity);
|
|
175
|
-
}
|
|
176
|
-
let intent: ArtifactRecoveryIntent | null;
|
|
177
|
-
try { intent = readArtifactRecoveryIntent(repoRoot, taskId, family, artifact); }
|
|
178
|
-
catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_SCHEMA_INVALID', identity); }
|
|
179
|
-
if (!intent || intent.state !== 'consumed') {
|
|
180
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'LFAI_STATE_PROTECTED', identity);
|
|
181
|
-
}
|
|
182
|
-
const artifactPath = path.join(task!.taskDir, artifact);
|
|
183
|
-
if (safeLstat(task!.taskDir, artifactPath, 'file') === null) {
|
|
184
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_IDENTITY_MISMATCH', identity);
|
|
185
|
-
}
|
|
186
|
-
let content: string;
|
|
187
|
-
try { content = fs.readFileSync(artifactPath, 'utf8'); }
|
|
188
|
-
catch { return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_UNREADABLE', identity); }
|
|
189
|
-
if (intent.finalArtifactSha256 !== sha256Content(content) || intent.finalSemanticDigest !== semanticDigest(content)) {
|
|
190
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'protected', 'ARTIFACT_DIGEST_MISMATCH', identity);
|
|
191
|
-
}
|
|
192
|
-
const receiptReason = receiptGate(repoRoot, task!, options);
|
|
193
|
-
if (receiptReason) return item('LFAI-CONSUMED', taskId, filePath, 'protected', receiptReason, identity);
|
|
194
|
-
return item('LFAI-CONSUMED', taskId, filePath, 'planned', 'LFAI_CONSUMED_AND_VERIFIED', identity);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
144
|
function commitIntentCandidate(
|
|
198
145
|
repoRoot: string,
|
|
199
146
|
task: TaskRecord | undefined,
|
|
@@ -258,23 +205,12 @@ function readAuxiliaryCandidates(
|
|
|
258
205
|
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
259
206
|
continue;
|
|
260
207
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (!
|
|
264
|
-
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
candidates.push(localIntentCandidate(
|
|
268
|
-
repoRoot, tasks.get(match[1]!), filePath, match[1]!, match[2] as 'analysis' | 'plan' | 'code', match[3]!, identity, options
|
|
269
|
-
));
|
|
270
|
-
} else {
|
|
271
|
-
const match = COMMIT_INTENT_RE.exec(entry.name);
|
|
272
|
-
if (!match || (selected && !selected.has(match[1]!))) {
|
|
273
|
-
if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]!), filePath, match[1]!, identity, options));
|
|
208
|
+
const match = COMMIT_INTENT_RE.exec(entry.name);
|
|
209
|
+
if (!match || (selected && !selected.has(match[1]!))) {
|
|
210
|
+
if (!selected || !match) candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
211
|
+
continue;
|
|
277
212
|
}
|
|
213
|
+
candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]!), filePath, match[1]!, identity, options));
|
|
278
214
|
}
|
|
279
215
|
if (entries.length === 0) {
|
|
280
216
|
candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
|
|
@@ -307,56 +243,7 @@ function scanIntermediateCleanup(repoRoot: string, options: ScanOptions = {}): I
|
|
|
307
243
|
return buildIntermediateCleanupReport(candidates, true);
|
|
308
244
|
}
|
|
309
245
|
|
|
310
|
-
function
|
|
311
|
-
if (candidate.kind !== 'LFAI-CONSUMED' || !candidate.taskId) return true;
|
|
312
|
-
if (!task) return false;
|
|
313
|
-
const match = LOCAL_INTENT_RE.exec(path.basename(candidate.path));
|
|
314
|
-
if (!match) return false;
|
|
315
|
-
let intent: ReturnType<typeof readArtifactRecoveryIntent>;
|
|
316
|
-
try { intent = readArtifactRecoveryIntent(repoRoot, candidate.taskId, match[2] as ArtifactSchemaFamily, match[3]!); }
|
|
317
|
-
catch { return false; }
|
|
318
|
-
if (!intent || intent.state !== 'consumed') return false;
|
|
319
|
-
|
|
320
|
-
const recovery = artifactRecoveryRoot(task.taskDir, intent.recoveryOperationId);
|
|
321
|
-
let stat: fs.Stats | null = null;
|
|
322
|
-
try { stat = fs.lstatSync(recovery); } catch (error) {
|
|
323
|
-
if (errorCode(error) === 'ENOENT') return true;
|
|
324
|
-
return false;
|
|
325
|
-
}
|
|
326
|
-
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
327
|
-
let entries: fs.Dirent[];
|
|
328
|
-
try { entries = fs.readdirSync(recovery, { withFileTypes: true }); } catch { return false; }
|
|
329
|
-
for (const entry of entries) {
|
|
330
|
-
if (entry.isSymbolicLink()) return false;
|
|
331
|
-
if (entry.isFile()) {
|
|
332
|
-
if (!['baseline.md', 'candidate.md', 'final.md', 'publish.md'].includes(entry.name)) return false;
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (!entry.isDirectory() || entry.name !== 'generations') return false;
|
|
336
|
-
let generations: fs.Dirent[];
|
|
337
|
-
try { generations = fs.readdirSync(path.join(recovery, entry.name), { withFileTypes: true }); } catch { return false; }
|
|
338
|
-
if (generations.some((generation) => generation.isSymbolicLink()
|
|
339
|
-
|| !generation.isFile() || !/^[a-f0-9]{64}\.md$/u.test(generation.name))) return false;
|
|
340
|
-
}
|
|
341
|
-
try {
|
|
342
|
-
for (const entry of entries) {
|
|
343
|
-
const target = path.join(recovery, entry.name);
|
|
344
|
-
if (entry.isFile()) fs.unlinkSync(target);
|
|
345
|
-
else {
|
|
346
|
-
for (const generation of fs.readdirSync(target, { withFileTypes: true })) fs.unlinkSync(path.join(target, generation.name));
|
|
347
|
-
fs.rmdirSync(target);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
fs.rmdirSync(recovery);
|
|
351
|
-
const recoveryRoot = path.dirname(recovery);
|
|
352
|
-
if (fs.readdirSync(recoveryRoot).length === 0) fs.rmdirSync(recoveryRoot);
|
|
353
|
-
return true;
|
|
354
|
-
} catch (error) {
|
|
355
|
-
return errorCode(error) === 'ENOENT';
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, task?: TaskRecord): IntermediateCleanupItem {
|
|
246
|
+
function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem): IntermediateCleanupItem {
|
|
360
247
|
if (candidate.disposition !== 'planned') return candidate;
|
|
361
248
|
const root = candidate.kind === 'EMPTY-AUX-PARENT'
|
|
362
249
|
? path.dirname(path.dirname(candidate.path))
|
|
@@ -373,9 +260,6 @@ function removeCandidate(repoRoot: string, candidate: IntermediateCleanupItem, t
|
|
|
373
260
|
const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
|
|
374
261
|
if (!actual) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
375
262
|
if (!sameFileIdentity(expected, actual)) return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
376
|
-
if (!cleanupConsumedRecoveryFiles(repoRoot, task, candidate)) {
|
|
377
|
-
return { ...candidate, disposition: 'protected', reason: 'RECOVERY_STAGING_NOT_SAFE_TO_REMOVE' };
|
|
378
|
-
}
|
|
379
263
|
try {
|
|
380
264
|
if (candidate.kind === 'EMPTY-AUX-PARENT') {
|
|
381
265
|
if (fs.readdirSync(candidate.path).length !== 0) return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
|
|
@@ -434,7 +318,7 @@ function cleanupIntermediateUnderRemovalCoordinator(
|
|
|
434
318
|
if (initialCandidate.disposition !== 'planned') continue;
|
|
435
319
|
const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
|
|
436
320
|
items[index] = current
|
|
437
|
-
? removeCandidate(repoRoot, current
|
|
321
|
+
? removeCandidate(repoRoot, current)
|
|
438
322
|
: { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
|
|
439
323
|
}
|
|
440
324
|
};
|
|
@@ -14,9 +14,11 @@ import type { LedgerRow, ReviewStage } from './ledger.ts';
|
|
|
14
14
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
15
15
|
import { writeTask } from './write.ts';
|
|
16
16
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import
|
|
17
|
+
import { parseLegacyReworkIntentDocument, parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
|
|
18
|
+
import type { ReworkClassification, ReworkIntent, ReworkTarget } from './rework-intent.ts';
|
|
19
|
+
import { extractSection, extractSubSection } from './sections.ts';
|
|
20
|
+
import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
21
|
+
import { scanVisibleMarkdown } from './markdown.ts';
|
|
20
22
|
|
|
21
23
|
type ReviewSeverity = 'blocker' | 'major' | 'minor';
|
|
22
24
|
type ExecutorResponse = 'accepted' | 'adjusted' | 'refuted' | 'cannot-judge';
|
|
@@ -27,7 +29,8 @@ type LedgerIntent =
|
|
|
27
29
|
| { kind: 'finding-review'; taskRef: string; id: string; status: ReviewDisposition; evidence: string; needsImplementation?: boolean; dryRun?: boolean }
|
|
28
30
|
| { kind: 'decision-next-id'; taskRef: string }
|
|
29
31
|
| { 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;
|
|
32
|
+
| { kind: 'rework-intent-upsert'; taskRef: string; intentId: string; findingId: string; sourceArtifact: string; sourceSha256: string; classification: ReworkClassification; dryRun?: boolean }
|
|
33
|
+
| { kind: 'rework-intent-rebuild'; taskRef: string; findingId?: string; sourceArtifact?: string; sourceSha256?: string; classification?: ReworkClassification; dryRun?: boolean };
|
|
31
34
|
|
|
32
35
|
type LedgerIntentError = { code: string; message: string };
|
|
33
36
|
type LedgerIntentResult = {
|
|
@@ -39,6 +42,7 @@ type LedgerIntentResult = {
|
|
|
39
42
|
before: LedgerRow | null;
|
|
40
43
|
after: LedgerRow | null;
|
|
41
44
|
operations: readonly TaskOperationSummary[];
|
|
45
|
+
rebuild?: Readonly<{ mode: 'format-only' | 'pending-reclassification'; beforeSha256: string; afterSha256: string; converted: number; newIntentId: string | null }>;
|
|
42
46
|
error: LedgerIntentError | null;
|
|
43
47
|
};
|
|
44
48
|
|
|
@@ -60,12 +64,53 @@ function ledgerReadErrorCode(error: unknown): string {
|
|
|
60
64
|
return code === 'TABLE_DUPLICATE_KEY' ? 'LEDGER_DUPLICATE_ID' : 'LEDGER_DOCUMENT_INVALID';
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const CLASSIFICATION_TARGET: Record<ReworkClassification, ReworkTarget> = {
|
|
68
|
+
'scope-or-requirement': 'analysis', design: 'plan', implementation: 'code',
|
|
69
|
+
'human-decision': 'pause', 'insufficient-evidence': 'pause'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function semanticDigest(value: string): string {
|
|
73
|
+
return createHash('sha256').update(value.normalize('NFKC').replace(/\s+/g, ' ').trim()).digest('hex');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizedTaskFact(value: string): string {
|
|
77
|
+
return value.normalize('NFKC').replace(/\s+/g, ' ').trim();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function semanticFindingEvidence(reviewContent: string, heading: ReturnType<typeof scanVisibleMarkdown>['headings'][number], end: number): string {
|
|
81
|
+
const title = heading.text.replace(/^\d+[.、::]\s*/, '').trim();
|
|
82
|
+
return `${title}\n${reviewContent.slice(heading.end, end)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function findingEvidence(reviewContent: string, evidence: string): string | null {
|
|
86
|
+
const anchor = evidence.split('#')[1];
|
|
87
|
+
if (!anchor) return null;
|
|
88
|
+
const markdown = scanVisibleMarkdown(reviewContent);
|
|
89
|
+
const explicit = markdown.anchors.find((candidate) => candidate.id === anchor);
|
|
90
|
+
if (explicit) {
|
|
91
|
+
const heading = markdown.headings.find((candidate) => candidate.start > explicit.start);
|
|
92
|
+
const next = heading && markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
|
|
93
|
+
const end = next?.start ?? reviewContent.length;
|
|
94
|
+
return heading ? semanticFindingEvidence(reviewContent, heading, end) : reviewContent.slice(explicit.end, end);
|
|
95
|
+
}
|
|
96
|
+
const heading = markdown.headings.find((candidate) => {
|
|
97
|
+
const escapedAnchor = anchor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
98
|
+
return candidate.text === anchor || new RegExp(`^${escapedAnchor}(?:\\s|[.、::])`).test(candidate.text);
|
|
99
|
+
});
|
|
100
|
+
if (!heading) return null;
|
|
101
|
+
const next = markdown.headings.find((candidate) => candidate.start > heading.start && candidate.level <= heading.level);
|
|
102
|
+
return semanticFindingEvidence(reviewContent, heading, next?.start ?? reviewContent.length);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function taskFactDigest(taskDir: string, content: string): string {
|
|
106
|
+
const analysisNames = fs.readdirSync(taskDir).filter((name) => /^analysis(?:-r\d+)?\.md$/.test(name));
|
|
107
|
+
const latest = analysisNames.map((name) => ({ name, round: parseArtifactName(name)?.round ?? 0 })).sort((a, b) => b.round - a.round)[0];
|
|
108
|
+
const flow = latest ? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, latest.name), 'utf8')) : null;
|
|
109
|
+
return semanticDigest(JSON.stringify({
|
|
110
|
+
taskInput: normalizedTaskFact(extractSection(content, ['任务输入', 'Task Input'])),
|
|
111
|
+
requirements: normalizedTaskFact(extractSection(content, ['需求', 'Requirements'])),
|
|
112
|
+
flow: flow?.status === 'valid' ? flow.decision.semanticDigest : flow?.status ?? 'missing'
|
|
113
|
+
}));
|
|
69
114
|
}
|
|
70
115
|
|
|
71
116
|
function nextFindingId(rows: readonly LedgerRow[], stage: ReviewStage): string {
|
|
@@ -103,8 +148,7 @@ function mapWrite(intent: LedgerIntent, entityId: string, before: LedgerRow | nu
|
|
|
103
148
|
function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {}): LedgerIntentResult {
|
|
104
149
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
105
150
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
106
|
-
|
|
107
|
-
if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
151
|
+
if (resolved.state !== 'active') return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
108
152
|
let content: string;
|
|
109
153
|
let rows: LedgerRow[];
|
|
110
154
|
try {
|
|
@@ -123,11 +167,14 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
123
167
|
}
|
|
124
168
|
|
|
125
169
|
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,
|
|
170
|
+
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)) {
|
|
171
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, classification, or source hash is invalid', resolved.taskId, intent.intentId);
|
|
128
172
|
}
|
|
129
173
|
const finding = rows.find((row) => row.id === intent.findingId);
|
|
130
174
|
if (!finding || finding.severity === 'decision') return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
|
|
175
|
+
if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
|
|
176
|
+
return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId, intent.findingId);
|
|
177
|
+
}
|
|
131
178
|
const sourceFromEvidence = finding.evidence.split('#')[0];
|
|
132
179
|
if (sourceFromEvidence !== intent.sourceArtifact || parseArtifactName(intent.sourceArtifact)?.family !== `review-${finding.stage}`) {
|
|
133
180
|
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
|
|
@@ -137,12 +184,34 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
137
184
|
if (actualHash !== intent.sourceSha256) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact hash does not match', resolved.taskId, intent.intentId);
|
|
138
185
|
const parsed = parseReworkIntentDocument(content);
|
|
139
186
|
if (!parsed.ok) return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
|
|
140
|
-
const
|
|
187
|
+
const reviewContent = fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact), 'utf8');
|
|
188
|
+
const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
|
|
189
|
+
if (!evidenceBlock) return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId, intent.intentId);
|
|
190
|
+
const evidenceDigest = semanticDigest(evidenceBlock);
|
|
191
|
+
const factsDigest = taskFactDigest(resolved.taskDir, content);
|
|
192
|
+
const requestedIntent: ReworkIntent = {
|
|
141
193
|
intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
|
|
142
|
-
sourceSha256: intent.sourceSha256, target: intent.
|
|
194
|
+
sourceSha256: intent.sourceSha256, target: CLASSIFICATION_TARGET[intent.classification], classification: intent.classification,
|
|
195
|
+
evidenceDigest, taskFactDigest: factsDigest, status: 'pending',
|
|
143
196
|
declaredAt: new Date().toISOString(), consumedAt: ''
|
|
144
197
|
};
|
|
145
|
-
|
|
198
|
+
if (parsed.intents.some((candidate) => candidate.intentId === intent.intentId)) {
|
|
199
|
+
const replay = upsertReworkIntent(parsed.intents, requestedIntent);
|
|
200
|
+
if (!replay.changed) {
|
|
201
|
+
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const same = parsed.intents.find((candidate) => candidate.findingId === intent.findingId
|
|
205
|
+
&& candidate.evidenceDigest === evidenceDigest && candidate.taskFactDigest === factsDigest
|
|
206
|
+
&& candidate.classification === intent.classification && candidate.target === CLASSIFICATION_TARGET[intent.classification]);
|
|
207
|
+
const classification: ReworkClassification = same ? 'insufficient-evidence' : intent.classification;
|
|
208
|
+
const nextIntent: ReworkIntent = {
|
|
209
|
+
...requestedIntent, target: CLASSIFICATION_TARGET[classification], classification
|
|
210
|
+
};
|
|
211
|
+
const now = nextIntent.declaredAt;
|
|
212
|
+
const superseded = parsed.intents.map((candidate) => candidate.status === 'pending'
|
|
213
|
+
? { ...candidate, status: 'superseded' as const, consumedAt: now } : candidate);
|
|
214
|
+
const next = upsertReworkIntent(superseded, nextIntent);
|
|
146
215
|
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
216
|
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
217
|
if (result.status === 'failed') return failed(intent, result.error.code, result.error.message, result.taskId, intent.intentId);
|
|
@@ -152,6 +221,75 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
152
221
|
}
|
|
153
222
|
}
|
|
154
223
|
|
|
224
|
+
if (intent.kind === 'rework-intent-rebuild') {
|
|
225
|
+
const beforeSha256 = createHash('sha256').update(content).digest('hex');
|
|
226
|
+
const supplied = [intent.findingId, intent.sourceArtifact, intent.sourceSha256, intent.classification].filter((value) => value !== undefined).length;
|
|
227
|
+
if (supplied !== 0 && supplied !== 4) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild binding options must be provided together', resolved.taskId);
|
|
228
|
+
const current = parseReworkIntentDocument(content);
|
|
229
|
+
if (current.ok && current.present) {
|
|
230
|
+
if (supplied !== 0) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'current rework intent format does not accept rebuild binding options', resolved.taskId);
|
|
231
|
+
return {
|
|
232
|
+
status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: null,
|
|
233
|
+
before: null, after: null, operations: [], error: null,
|
|
234
|
+
rebuild: { mode: 'format-only', beforeSha256, afterSha256: beforeSha256, converted: current.intents.length, newIntentId: null }
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const legacy = parseLegacyReworkIntentDocument(content);
|
|
238
|
+
if (!legacy.ok) return failed(intent, legacy.code, legacy.message, resolved.taskId);
|
|
239
|
+
const pending = legacy.intents.filter((row) => row.status === 'pending');
|
|
240
|
+
if (pending.length === 0 && supplied !== 0) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'format-only rebuild does not accept finding or classification options', resolved.taskId);
|
|
241
|
+
if (pending.length > 0 && supplied !== 4) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'legacy pending intents require finding, review, hash, and classification', resolved.taskId);
|
|
242
|
+
const now = new Date().toISOString();
|
|
243
|
+
let next = legacy.intents.map((row) => row.status === 'pending' ? { ...row, status: 'superseded' as const, consumedAt: now } : row);
|
|
244
|
+
let entityId: string | null = null;
|
|
245
|
+
if (pending.length > 0) {
|
|
246
|
+
const finding = rows.find((row) => row.id === intent.findingId);
|
|
247
|
+
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);
|
|
248
|
+
if (intent.classification === 'human-decision' && finding.status !== 'needs-human-decision') {
|
|
249
|
+
return failed(intent, 'LEDGER_TRANSITION_INVALID', 'human-decision classification requires a needs-human-decision finding', resolved.taskId);
|
|
250
|
+
}
|
|
251
|
+
const sourceIdentity = parseArtifactName(intent.sourceArtifact!);
|
|
252
|
+
const expectedFamily = `review-${finding.stage}`;
|
|
253
|
+
const latestSource = fs.readdirSync(resolved.taskDir)
|
|
254
|
+
.map((name) => parseArtifactName(name))
|
|
255
|
+
.filter((identity) => identity?.family === expectedFamily)
|
|
256
|
+
.sort((left, right) => right!.round - left!.round || left!.name.localeCompare(right!.name))[0];
|
|
257
|
+
if (!sourceIdentity || sourceIdentity.family !== expectedFamily || latestSource?.name !== sourceIdentity.name) {
|
|
258
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact is not the latest review for the finding stage', resolved.taskId);
|
|
259
|
+
}
|
|
260
|
+
const sourcePath = path.join(resolved.taskDir, intent.sourceArtifact!);
|
|
261
|
+
const actualHash = createHash('sha256').update(fs.readFileSync(sourcePath)).digest('hex');
|
|
262
|
+
if (actualHash !== intent.sourceSha256) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rebuild source artifact hash does not match', resolved.taskId);
|
|
263
|
+
const max = next.reduce((value, row) => Math.max(value, Number(row.intentId.slice(3))), 0);
|
|
264
|
+
entityId = `RI-${max + 1}`;
|
|
265
|
+
const reviewContent = fs.readFileSync(sourcePath, 'utf8');
|
|
266
|
+
const classification = intent.classification!;
|
|
267
|
+
if (!(classification in CLASSIFICATION_TARGET)) return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rebuild classification is invalid', resolved.taskId);
|
|
268
|
+
const evidenceBlock = findingEvidence(reviewContent, finding.evidence);
|
|
269
|
+
if (!evidenceBlock) return failed(intent, 'LEDGER_EVIDENCE_INVALID', `finding evidence anchor '${finding.evidence}' was not found`, resolved.taskId);
|
|
270
|
+
next = [...next, {
|
|
271
|
+
intentId: entityId, findingId: intent.findingId!, sourceArtifact: intent.sourceArtifact!, sourceSha256: intent.sourceSha256!,
|
|
272
|
+
target: CLASSIFICATION_TARGET[classification], classification,
|
|
273
|
+
evidenceDigest: semanticDigest(evidenceBlock),
|
|
274
|
+
taskFactDigest: taskFactDigest(resolved.taskDir, content), status: 'pending', declaredAt: now, consumedAt: ''
|
|
275
|
+
}];
|
|
276
|
+
}
|
|
277
|
+
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 } });
|
|
278
|
+
if (result.status === 'failed') return failed(intent, result.error.code, result.error.message, result.taskId, entityId);
|
|
279
|
+
const afterContent = result.status === 'applied' || result.status === 'no-op' ? fs.readFileSync(resolved.taskMdPath, 'utf8') : content;
|
|
280
|
+
return {
|
|
281
|
+
status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId,
|
|
282
|
+
before: null, after: null, operations: result.operations, error: null,
|
|
283
|
+
rebuild: {
|
|
284
|
+
mode: pending.length > 0 ? 'pending-reclassification' : 'format-only',
|
|
285
|
+
beforeSha256,
|
|
286
|
+
afterSha256: createHash('sha256').update(afterContent).digest('hex'),
|
|
287
|
+
converted: legacy.intents.length,
|
|
288
|
+
newIntentId: entityId
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
155
293
|
let before: LedgerRow | null = null;
|
|
156
294
|
let after: LedgerRow;
|
|
157
295
|
if (intent.kind === 'finding-upsert') {
|
|
@@ -213,8 +351,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
213
351
|
? new Set(['confirmed', 'open', 'needs-human-decision'])
|
|
214
352
|
: before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set<string>();
|
|
215
353
|
const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
|
|
216
|
-
if (
|
|
217
|
-
&& !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
|
|
354
|
+
if (!sameRoundMinorClose && !allowed.has(intent.status)) {
|
|
218
355
|
return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
|
|
219
356
|
}
|
|
220
357
|
after = { ...before, status: intent.status, evidence };
|
|
@@ -248,7 +385,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
248
385
|
}
|
|
249
386
|
|
|
250
387
|
const result = writeTask({
|
|
251
|
-
taskRef: intent.taskRef, expectedState:
|
|
388
|
+
taskRef: intent.taskRef, expectedState: 'active',
|
|
252
389
|
mutations: [rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
|
|
253
390
|
dryRun: 'dryRun' in intent ? intent.dryRun : false
|
|
254
391
|
}, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
@@ -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 };
|