patchwarden 0.6.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +18 -18
- package/README.md +20 -20
- package/dist/agents/agentRouter.d.ts +40 -0
- package/dist/agents/agentRouter.js +95 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +6 -2
- package/dist/controlCenter.js +15 -2
- package/dist/doctor.js +32 -1
- package/dist/goal/acceptanceEngine.d.ts +62 -0
- package/dist/goal/acceptanceEngine.js +103 -0
- package/dist/goal/acceptanceTemplate.d.ts +10 -0
- package/dist/goal/acceptanceTemplate.js +104 -0
- package/dist/goal/goalGraph.d.ts +58 -0
- package/dist/goal/goalGraph.js +189 -0
- package/dist/goal/goalProgress.d.ts +81 -0
- package/dist/goal/goalProgress.js +216 -0
- package/dist/goal/goalStatus.d.ts +60 -0
- package/dist/goal/goalStatus.js +137 -0
- package/dist/goal/goalStore.d.ts +79 -0
- package/dist/goal/goalStore.js +211 -0
- package/dist/goal/handoffExport.d.ts +34 -0
- package/dist/goal/handoffExport.js +183 -0
- package/dist/goal/subgoalSync.d.ts +40 -0
- package/dist/goal/subgoalSync.js +72 -0
- package/dist/goal/worktreeManager.d.ts +96 -0
- package/dist/goal/worktreeManager.js +292 -0
- package/dist/logging.d.ts +44 -0
- package/dist/logging.js +68 -0
- package/dist/release/releaseGate.d.ts +99 -0
- package/dist/release/releaseGate.js +475 -0
- package/dist/runner/postTaskCleanup.d.ts +13 -0
- package/dist/runner/postTaskCleanup.js +147 -0
- package/dist/runner/runTask.js +50 -10
- package/dist/security/discoveryTokenStore.d.ts +62 -0
- package/dist/security/discoveryTokenStore.js +100 -0
- package/dist/security/toolInvocationGuard.d.ts +35 -0
- package/dist/security/toolInvocationGuard.js +153 -0
- package/dist/smoke-test.js +18 -10
- package/dist/taskRuntime.d.ts +17 -0
- package/dist/test/unit/acceptance-engine.test.d.ts +1 -0
- package/dist/test/unit/acceptance-engine.test.js +228 -0
- package/dist/test/unit/agent-router.test.d.ts +1 -0
- package/dist/test/unit/agent-router.test.js +287 -0
- package/dist/test/unit/audit-checks.test.d.ts +1 -0
- package/dist/test/unit/audit-checks.test.js +350 -0
- package/dist/test/unit/diagnose-task.test.d.ts +1 -0
- package/dist/test/unit/diagnose-task.test.js +457 -0
- package/dist/test/unit/discovery-token-store.test.d.ts +1 -0
- package/dist/test/unit/discovery-token-store.test.js +139 -0
- package/dist/test/unit/goal-graph.test.d.ts +1 -0
- package/dist/test/unit/goal-graph.test.js +298 -0
- package/dist/test/unit/goal-progress.test.d.ts +1 -0
- package/dist/test/unit/goal-progress.test.js +381 -0
- package/dist/test/unit/goal-status.test.d.ts +1 -0
- package/dist/test/unit/goal-status.test.js +215 -0
- package/dist/test/unit/goal-store.test.d.ts +1 -0
- package/dist/test/unit/goal-store.test.js +253 -0
- package/dist/test/unit/goal-subgoal-task.test.d.ts +1 -0
- package/dist/test/unit/goal-subgoal-task.test.js +55 -0
- package/dist/test/unit/goal-tools-registry.test.d.ts +1 -0
- package/dist/test/unit/goal-tools-registry.test.js +190 -0
- package/dist/test/unit/handoff-export.test.d.ts +1 -0
- package/dist/test/unit/handoff-export.test.js +263 -0
- package/dist/test/unit/invoke-discovered-tool.test.d.ts +1 -0
- package/dist/test/unit/invoke-discovered-tool.test.js +167 -0
- package/dist/test/unit/logging.test.js +127 -5
- package/dist/test/unit/post-task-cleanup.test.d.ts +1 -0
- package/dist/test/unit/post-task-cleanup.test.js +48 -0
- package/dist/test/unit/reconcile-tasks.test.d.ts +1 -0
- package/dist/test/unit/reconcile-tasks.test.js +456 -0
- package/dist/test/unit/release-gate.test.d.ts +1 -0
- package/dist/test/unit/release-gate.test.js +242 -0
- package/dist/test/unit/safe-views.test.d.ts +1 -0
- package/dist/test/unit/safe-views.test.js +171 -0
- package/dist/test/unit/schema-drift-check.test.d.ts +1 -0
- package/dist/test/unit/schema-drift-check.test.js +175 -0
- package/dist/test/unit/subgoal-sync.test.d.ts +1 -0
- package/dist/test/unit/subgoal-sync.test.js +183 -0
- package/dist/test/unit/tool-invocation-guard.test.d.ts +1 -0
- package/dist/test/unit/tool-invocation-guard.test.js +432 -0
- package/dist/test/unit/tool-usage-stats.test.d.ts +1 -0
- package/dist/test/unit/tool-usage-stats.test.js +300 -0
- package/dist/test/unit/toolSearch.test.d.ts +1 -0
- package/dist/test/unit/toolSearch.test.js +571 -0
- package/dist/test/unit/worktree-manager.test.d.ts +1 -0
- package/dist/test/unit/worktree-manager.test.js +176 -0
- package/dist/tools/auditTask.d.ts +103 -1
- package/dist/tools/auditTask.js +461 -8
- package/dist/tools/cancelTask.js +1 -1
- package/dist/tools/checkReleaseGate.d.ts +21 -0
- package/dist/tools/checkReleaseGate.js +22 -0
- package/dist/tools/createTask.d.ts +18 -2
- package/dist/tools/createTask.js +29 -1
- package/dist/tools/diagnoseTask.d.ts +45 -0
- package/dist/tools/diagnoseTask.js +347 -0
- package/dist/tools/discardWorktree.d.ts +24 -0
- package/dist/tools/discardWorktree.js +27 -0
- package/dist/tools/discoverTools.d.ts +11 -0
- package/dist/tools/discoverTools.js +39 -0
- package/dist/tools/explainTool.d.ts +11 -0
- package/dist/tools/explainTool.js +21 -0
- package/dist/tools/getTaskSummary.js +2 -1
- package/dist/tools/goalSubgoalTask.d.ts +51 -0
- package/dist/tools/goalSubgoalTask.js +110 -0
- package/dist/tools/healthCheck.d.ts +1 -0
- package/dist/tools/healthCheck.js +5 -1
- package/dist/tools/invokeDiscoveredTool.d.ts +37 -0
- package/dist/tools/invokeDiscoveredTool.js +112 -0
- package/dist/tools/listTasks.d.ts +3 -1
- package/dist/tools/listTasks.js +14 -1
- package/dist/tools/mergeWorktree.d.ts +24 -0
- package/dist/tools/mergeWorktree.js +27 -0
- package/dist/tools/reconcileTasks.d.ts +41 -0
- package/dist/tools/reconcileTasks.js +352 -0
- package/dist/tools/registry.js +550 -0
- package/dist/tools/safeStatus.d.ts +31 -1
- package/dist/tools/safeStatus.js +43 -1
- package/dist/tools/safeViews.d.ts +256 -0
- package/dist/tools/safeViews.js +250 -0
- package/dist/tools/schemaDriftCheck.d.ts +46 -0
- package/dist/tools/schemaDriftCheck.js +80 -0
- package/dist/tools/toolCatalog.d.ts +4 -3
- package/dist/tools/toolCatalog.js +33 -11
- package/dist/tools/toolRegistry.d.ts +61 -0
- package/dist/tools/toolRegistry.js +724 -0
- package/dist/tools/toolSearch.d.ts +110 -0
- package/dist/tools/toolSearch.js +331 -0
- package/dist/tools/toolUsageStats.d.ts +41 -0
- package/dist/tools/toolUsageStats.js +116 -0
- package/dist/tools/waitForTask.js +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/watcherStatus.d.ts +29 -0
- package/dist/watcherStatus.js +92 -1
- package/package.json +3 -2
- package/scripts/checks/control-smoke.js +2 -2
- package/scripts/checks/doctor-smoke.js +1 -0
- package/scripts/checks/lifecycle-smoke.js +13 -9
- package/scripts/checks/mcp-manifest-check.js +53 -6
- package/scripts/checks/mcp-smoke.js +36 -9
- package/scripts/checks/watcher-supervisor-smoke.js +2 -2
- package/src/agents/agentRouter.ts +149 -0
- package/src/config.ts +9 -3
- package/src/controlCenter.ts +17 -3
- package/src/doctor.ts +36 -1
- package/src/goal/acceptanceEngine.ts +160 -0
- package/src/goal/acceptanceTemplate.ts +121 -0
- package/src/goal/goalGraph.ts +225 -0
- package/src/goal/goalProgress.ts +301 -0
- package/src/goal/goalStatus.ts +234 -0
- package/src/goal/goalStore.ts +306 -0
- package/src/goal/handoffExport.ts +211 -0
- package/src/goal/subgoalSync.ts +82 -0
- package/src/goal/worktreeManager.ts +404 -0
- package/src/logging.ts +91 -0
- package/src/release/releaseGate.ts +567 -0
- package/src/runner/postTaskCleanup.ts +154 -0
- package/src/runner/runTask.ts +49 -10
- package/src/security/discoveryTokenStore.ts +157 -0
- package/src/security/toolInvocationGuard.ts +251 -0
- package/src/smoke-test.ts +15 -7
- package/src/taskRuntime.ts +17 -0
- package/src/test/unit/acceptance-engine.test.ts +261 -0
- package/src/test/unit/agent-router.test.ts +342 -0
- package/src/test/unit/audit-checks.test.ts +567 -0
- package/src/test/unit/diagnose-task.test.ts +544 -0
- package/src/test/unit/discovery-token-store.test.ts +181 -0
- package/src/test/unit/goal-graph.test.ts +347 -0
- package/src/test/unit/goal-progress.test.ts +538 -0
- package/src/test/unit/goal-status.test.ts +270 -0
- package/src/test/unit/goal-store.test.ts +318 -0
- package/src/test/unit/goal-subgoal-task.test.ts +72 -0
- package/src/test/unit/goal-tools-registry.test.ts +243 -0
- package/src/test/unit/handoff-export.test.ts +295 -0
- package/src/test/unit/invoke-discovered-tool.test.ts +216 -0
- package/src/test/unit/logging.test.ts +177 -5
- package/src/test/unit/post-task-cleanup.test.ts +53 -0
- package/src/test/unit/reconcile-tasks.test.ts +551 -0
- package/src/test/unit/release-gate.test.ts +314 -0
- package/src/test/unit/safe-views.test.ts +184 -0
- package/src/test/unit/schema-drift-check.test.ts +258 -0
- package/src/test/unit/subgoal-sync.test.ts +236 -0
- package/src/test/unit/tool-invocation-guard.test.ts +542 -0
- package/src/test/unit/tool-usage-stats.test.ts +384 -0
- package/src/test/unit/toolSearch.test.ts +756 -0
- package/src/test/unit/worktree-manager.test.ts +247 -0
- package/src/tools/auditTask.ts +831 -402
- package/src/tools/cancelTask.ts +1 -1
- package/src/tools/checkReleaseGate.ts +45 -0
- package/src/tools/createTask.ts +64 -6
- package/src/tools/diagnoseTask.ts +460 -0
- package/src/tools/discardWorktree.ts +42 -0
- package/src/tools/discoverTools.ts +51 -0
- package/src/tools/explainTool.ts +26 -0
- package/src/tools/getTaskSummary.ts +2 -1
- package/src/tools/goalSubgoalTask.ts +170 -0
- package/src/tools/healthCheck.ts +4 -1
- package/src/tools/invokeDiscoveredTool.ts +163 -0
- package/src/tools/listTasks.ts +16 -2
- package/src/tools/mergeWorktree.ts +42 -0
- package/src/tools/reconcileTasks.ts +464 -0
- package/src/tools/registry.ts +618 -1
- package/src/tools/safeStatus.ts +73 -2
- package/src/tools/safeViews.ts +271 -0
- package/src/tools/schemaDriftCheck.ts +120 -0
- package/src/tools/toolCatalog.ts +34 -11
- package/src/tools/toolRegistry.ts +786 -0
- package/src/tools/toolSearch.ts +464 -0
- package/src/tools/toolUsageStats.ts +151 -0
- package/src/tools/waitForTask.ts +1 -0
- package/src/version.ts +2 -2
- package/src/watcherStatus.ts +97 -1
package/src/controlCenter.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* PatchWarden Control Center — local HTTP dashboard service.
|
|
4
4
|
*
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type PatchWardenConfig,
|
|
28
28
|
} from "./config.js";
|
|
29
29
|
import { listTasks, type TaskEntry } from "./tools/listTasks.js";
|
|
30
|
+
import type { AcceptanceStatus } from "./tools/createTask.js";
|
|
30
31
|
import { listAgents, type AgentAvailability } from "./tools/listAgents.js";
|
|
31
32
|
import { readWatcherStatus, type WatcherStatusSnapshot } from "./watcherStatus.js";
|
|
32
33
|
import { redactSensitiveContent } from "./security/contentRedaction.js";
|
|
@@ -464,6 +465,7 @@ interface StaleClassification {
|
|
|
464
465
|
|
|
465
466
|
const TERMINAL_TASK_STATUSES = new Set([
|
|
466
467
|
"done",
|
|
468
|
+
"done_by_agent",
|
|
467
469
|
"failed",
|
|
468
470
|
"failed_verification",
|
|
469
471
|
"failed_scope_violation",
|
|
@@ -1049,13 +1051,19 @@ function handleReconcile(res: ServerResponse, taskId: string): void {
|
|
|
1049
1051
|
const runtimeData = readJsonFileSafe<Record<string, unknown>>(runtimePath) ?? {};
|
|
1050
1052
|
|
|
1051
1053
|
const watcher = readWatcherStatusSafe();
|
|
1054
|
+
const VALID_ACCEPTANCE = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
|
|
1055
|
+
const taskStatus = String(statusData.status || "pending");
|
|
1056
|
+
const taskAcceptanceStatus = taskStatus === "done_by_agent"
|
|
1057
|
+
? (typeof statusData.acceptance_status === "string" && VALID_ACCEPTANCE.includes(statusData.acceptance_status) ? (statusData.acceptance_status as AcceptanceStatus) : "pending" as AcceptanceStatus)
|
|
1058
|
+
: null;
|
|
1052
1059
|
const taskEntry: TaskEntry = {
|
|
1053
1060
|
task_id: taskId,
|
|
1054
1061
|
plan_id: String(statusData.plan_id || ""),
|
|
1055
1062
|
title: "",
|
|
1056
1063
|
agent: String(statusData.agent || ""),
|
|
1057
|
-
status:
|
|
1064
|
+
status: taskStatus as TaskEntry["status"],
|
|
1058
1065
|
phase: String(runtimeData.phase || statusData.phase || "queued") as TaskEntry["phase"],
|
|
1066
|
+
acceptance_status: taskAcceptanceStatus,
|
|
1059
1067
|
created_at: String(statusData.created_at || ""),
|
|
1060
1068
|
updated_at: String(statusData.updated_at || ""),
|
|
1061
1069
|
workspace_root: String(statusData.workspace_root || config.workspaceRoot),
|
|
@@ -1199,13 +1207,19 @@ function handleTaskDetail(res: ServerResponse, taskId: string): void {
|
|
|
1199
1207
|
let stale: StaleClassification | null = null;
|
|
1200
1208
|
if (statusData) {
|
|
1201
1209
|
const watcher = readWatcherStatusSafe();
|
|
1210
|
+
const VALID_ACCEPTANCE2 = ["pending", "accepted", "rejected", "needs_fix", "blocked"];
|
|
1211
|
+
const taskStatus2 = String(statusData.status || "pending");
|
|
1212
|
+
const taskAcceptanceStatus2 = taskStatus2 === "done_by_agent"
|
|
1213
|
+
? (typeof statusData.acceptance_status === "string" && VALID_ACCEPTANCE2.includes(statusData.acceptance_status) ? (statusData.acceptance_status as AcceptanceStatus) : "pending" as AcceptanceStatus)
|
|
1214
|
+
: null;
|
|
1202
1215
|
const entry: TaskEntry = {
|
|
1203
1216
|
task_id: taskId,
|
|
1204
1217
|
plan_id: String(statusData.plan_id || ""),
|
|
1205
1218
|
title: "",
|
|
1206
1219
|
agent: String(statusData.agent || ""),
|
|
1207
|
-
status:
|
|
1220
|
+
status: taskStatus2 as TaskEntry["status"],
|
|
1208
1221
|
phase: String(runtimeData?.phase || statusData.phase || "queued") as TaskEntry["phase"],
|
|
1222
|
+
acceptance_status: taskAcceptanceStatus2,
|
|
1209
1223
|
created_at: String(statusData.created_at || ""),
|
|
1210
1224
|
updated_at: String(statusData.updated_at || ""),
|
|
1211
1225
|
workspace_root: String(statusData.workspace_root || config.workspaceRoot),
|
package/src/doctor.ts
CHANGED
|
@@ -19,6 +19,9 @@ import { guardPlanContent } from "./security/planGuard.js";
|
|
|
19
19
|
import { TASK_READ_ONLY_FILES } from "./tools/getTaskFile.js";
|
|
20
20
|
import { getToolDefs } from "./tools/registry.js";
|
|
21
21
|
import { CHATGPT_CORE_TOOL_NAMES, CHATGPT_DIRECT_TOOL_NAMES, selectToolsForProfile } from "./tools/toolCatalog.js";
|
|
22
|
+
import { buildToolRegistry } from "./tools/toolRegistry.js";
|
|
23
|
+
import { runAllSchemaDriftChecks } from "./tools/schemaDriftCheck.js";
|
|
24
|
+
import { runReleaseGateCheck } from "./release/releaseGate.js";
|
|
22
25
|
import { PATCHWARDEN_VERSION } from "./version.js";
|
|
23
26
|
|
|
24
27
|
// ── State ──────────────────────────────────────────────────────────
|
|
@@ -245,7 +248,7 @@ try {
|
|
|
245
248
|
const coreTools = selectToolsForProfile(fullTools, "chatgpt_core", config?.enableDirectProfile);
|
|
246
249
|
const createSchema = coreTools.find((tool) => tool.name === "create_task")?.inputSchema as any;
|
|
247
250
|
const waitSchema = coreTools.find((tool) => tool.name === "wait_for_task")?.inputSchema as any;
|
|
248
|
-
check("Full tool profile exposes
|
|
251
|
+
check("Full tool profile exposes 54 tools", fullTools.length === 54, `${fullTools.length} tools`);
|
|
249
252
|
check(
|
|
250
253
|
`chatgpt_core profile exposes the exact ${CHATGPT_CORE_TOOL_NAMES.length}-tool manifest`,
|
|
251
254
|
JSON.stringify(coreTools.map((tool) => tool.name)) === JSON.stringify(CHATGPT_CORE_TOOL_NAMES),
|
|
@@ -280,11 +283,43 @@ try {
|
|
|
280
283
|
results.push(`[OK] chatgpt_direct enabled check skipped (enableDirectProfile=false)`);
|
|
281
284
|
ok++;
|
|
282
285
|
}
|
|
286
|
+
|
|
287
|
+
// 16. Schema drift 检查(v0.9.0)—— warn 级别,不阻断 doctor:ci
|
|
288
|
+
// 用 fullTools 同时构建 registry 和 toolDefs Map,确保 schema digest 比对基准一致。
|
|
289
|
+
const driftRegistry = buildToolRegistry(fullTools);
|
|
290
|
+
const driftToolDefs = new Map<string, { inputSchema: unknown }>();
|
|
291
|
+
for (const tool of fullTools) {
|
|
292
|
+
driftToolDefs.set(tool.name, { inputSchema: tool.inputSchema });
|
|
293
|
+
}
|
|
294
|
+
const driftResult = runAllSchemaDriftChecks(driftRegistry, driftToolDefs);
|
|
295
|
+
if (driftResult.ok) {
|
|
296
|
+
results.push(`[OK] Schema drift check — no drift detected`);
|
|
297
|
+
ok++;
|
|
298
|
+
} else {
|
|
299
|
+
// 每个 warning 单独输出为 WARN,与其他自检项的多警告模式一致
|
|
300
|
+
for (const w of driftResult.warnings) {
|
|
301
|
+
results.push(`[WARN] Schema drift: ${w}`);
|
|
302
|
+
warn++;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
283
305
|
} finally {
|
|
284
306
|
if (previousProfile === undefined) delete process.env.PATCHWARDEN_TOOL_PROFILE;
|
|
285
307
|
else process.env.PATCHWARDEN_TOOL_PROFILE = previousProfile;
|
|
286
308
|
}
|
|
287
309
|
|
|
310
|
+
// 17. Release gate module loadable (v1.0.0) — module integrity only.
|
|
311
|
+
// Does NOT execute local_ready (would recurse into doctor:ci).
|
|
312
|
+
try {
|
|
313
|
+
const releaseGateReady = typeof runReleaseGateCheck === "function";
|
|
314
|
+
check(
|
|
315
|
+
"Release gate module loadable",
|
|
316
|
+
releaseGateReady,
|
|
317
|
+
releaseGateReady ? "runReleaseGateCheck exported" : "runReleaseGateCheck missing or invalid",
|
|
318
|
+
);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
check("Release gate module loadable", false, error instanceof Error ? error.message : String(error));
|
|
321
|
+
}
|
|
322
|
+
|
|
288
323
|
// 9. HTTP port check
|
|
289
324
|
const httpPort = (config as any)?.http?.port || 7331;
|
|
290
325
|
try {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.7.2: 验收引擎 — 基于证据推导 4 级验收结论。
|
|
3
|
+
*
|
|
4
|
+
* 输入:status.json、task_meta(goal/scope/forbidden/verification/done_evidence)、
|
|
5
|
+
* audit checks(pass/warn/fail)、release claims、artifact 信息
|
|
6
|
+
* 输出:ACCEPTED / NEEDS_FIX / REJECTED / BLOCKED_BY_APPROVAL
|
|
7
|
+
* + reasons + required_evidence + next_suggested_task
|
|
8
|
+
*
|
|
9
|
+
* 决策规则:
|
|
10
|
+
* 1. 存在 fail 级检查项 → REJECTED
|
|
11
|
+
* 2. 存在 release claims 但无法验证远端 → BLOCKED_BY_APPROVAL
|
|
12
|
+
* 3. 存在 warn 级检查项或文档不同步 → NEEDS_FIX
|
|
13
|
+
* 4. 所有检查通过 → ACCEPTED
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { AcceptanceStatus } from "../tools/createTask.js";
|
|
17
|
+
|
|
18
|
+
// ── 类型定义 ──────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export type AcceptanceVerdict = "ACCEPTED" | "NEEDS_FIX" | "REJECTED" | "BLOCKED_BY_APPROVAL";
|
|
21
|
+
|
|
22
|
+
export interface AcceptanceCheck {
|
|
23
|
+
name: string;
|
|
24
|
+
result: "pass" | "warn" | "fail";
|
|
25
|
+
detail: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AcceptanceEvidence {
|
|
29
|
+
task_id: string;
|
|
30
|
+
task_status: string;
|
|
31
|
+
// 证据文件是否存在
|
|
32
|
+
result_md_exists: boolean;
|
|
33
|
+
result_json_exists: boolean;
|
|
34
|
+
verify_json_exists: boolean;
|
|
35
|
+
test_log_exists: boolean;
|
|
36
|
+
git_diff_exists: boolean;
|
|
37
|
+
// 验证状态
|
|
38
|
+
verify_status: "passed" | "failed" | "skipped" | null;
|
|
39
|
+
// 范围违规
|
|
40
|
+
new_out_of_scope_changes: Array<{ path: string; change: string }>;
|
|
41
|
+
// 任务元数据(来自 create_task 的结构化验收标准)
|
|
42
|
+
goal: string | null;
|
|
43
|
+
scope: string[] | null;
|
|
44
|
+
forbidden: string[] | null;
|
|
45
|
+
verification: string[] | null;
|
|
46
|
+
done_evidence: string[] | null;
|
|
47
|
+
// artifact 信息
|
|
48
|
+
artifact_status: string | null;
|
|
49
|
+
// release claims(auditTask 扫描到的发布声明)
|
|
50
|
+
release_claims_unverified: boolean;
|
|
51
|
+
// audit checks(来自 auditTask 的完整检查列表)
|
|
52
|
+
checks: AcceptanceCheck[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AcceptanceResult {
|
|
56
|
+
verdict: AcceptanceVerdict;
|
|
57
|
+
acceptance_status: AcceptanceStatus;
|
|
58
|
+
reason: string;
|
|
59
|
+
reasons: string[];
|
|
60
|
+
required_evidence: string[];
|
|
61
|
+
next_suggested_task: string;
|
|
62
|
+
fail_checks: AcceptanceCheck[];
|
|
63
|
+
warn_checks: AcceptanceCheck[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── 验收决策 ──────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 核心验收决策函数。
|
|
70
|
+
* 输入所有证据,输出 4 级验收结论。
|
|
71
|
+
*/
|
|
72
|
+
export function evaluateAcceptance(evidence: AcceptanceEvidence): AcceptanceResult {
|
|
73
|
+
const reasons: string[] = [];
|
|
74
|
+
const requiredEvidence: string[] = [];
|
|
75
|
+
const failChecks: AcceptanceCheck[] = [];
|
|
76
|
+
const warnChecks: AcceptanceCheck[] = [];
|
|
77
|
+
|
|
78
|
+
// 收集 fail 和 warn 检查项
|
|
79
|
+
for (const check of evidence.checks) {
|
|
80
|
+
if (check.result === "fail") {
|
|
81
|
+
failChecks.push(check);
|
|
82
|
+
} else if (check.result === "warn") {
|
|
83
|
+
warnChecks.push(check);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Rule 1: fail 级检查项 → REJECTED ──────────────────────
|
|
88
|
+
if (failChecks.length > 0) {
|
|
89
|
+
for (const check of failChecks) {
|
|
90
|
+
reasons.push(`[FAIL] ${check.name}: ${check.detail}`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
verdict: "REJECTED",
|
|
94
|
+
acceptance_status: "rejected",
|
|
95
|
+
reason: `${failChecks.length} fail-level check(s): ${failChecks.map((c) => c.name).join(", ")}`,
|
|
96
|
+
reasons,
|
|
97
|
+
required_evidence: requiredEvidence.length > 0 ? requiredEvidence : ["Fix all fail-level issues and re-run audit_task"],
|
|
98
|
+
next_suggested_task: `Fix fail-level issues: ${failChecks.map((c) => c.name).join(", ")}`,
|
|
99
|
+
fail_checks: failChecks,
|
|
100
|
+
warn_checks: warnChecks,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Rule 2: release claims 无法验证 → BLOCKED_BY_APPROVAL ──
|
|
105
|
+
if (evidence.release_claims_unverified) {
|
|
106
|
+
reasons.push("[BLOCKED] Release claims detected but remote state cannot be verified");
|
|
107
|
+
requiredEvidence.push("Verify remote release state (npm publish, GitHub release, git tag)");
|
|
108
|
+
return {
|
|
109
|
+
verdict: "BLOCKED_BY_APPROVAL",
|
|
110
|
+
acceptance_status: "blocked",
|
|
111
|
+
reason: "Release claims require manual verification of remote state",
|
|
112
|
+
reasons,
|
|
113
|
+
required_evidence: requiredEvidence,
|
|
114
|
+
next_suggested_task: "Manually verify remote release state, then re-run audit_task",
|
|
115
|
+
fail_checks: failChecks,
|
|
116
|
+
warn_checks: warnChecks,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Rule 3: warn 级检查项 → NEEDS_FIX ──────────────────────
|
|
121
|
+
if (warnChecks.length > 0) {
|
|
122
|
+
for (const check of warnChecks) {
|
|
123
|
+
reasons.push(`[WARN] ${check.name}: ${check.detail}`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
verdict: "NEEDS_FIX",
|
|
127
|
+
acceptance_status: "needs_fix",
|
|
128
|
+
reason: `${warnChecks.length} warn-level check(s) require fixes: ${warnChecks.map((c) => c.name).join(", ")}`,
|
|
129
|
+
reasons,
|
|
130
|
+
required_evidence: requiredEvidence.length > 0 ? requiredEvidence : ["Resolve all warn-level issues"],
|
|
131
|
+
next_suggested_task: `Fix warn-level issues: ${warnChecks.map((c) => c.name).join(", ")}`,
|
|
132
|
+
fail_checks: failChecks,
|
|
133
|
+
warn_checks: warnChecks,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Rule 4: 所有检查通过 → ACCEPTED ───────────────────────
|
|
138
|
+
return {
|
|
139
|
+
verdict: "ACCEPTED",
|
|
140
|
+
acceptance_status: "accepted",
|
|
141
|
+
reason: "All acceptance checks passed",
|
|
142
|
+
reasons: ["All checks passed, no warnings or failures"],
|
|
143
|
+
required_evidence: [],
|
|
144
|
+
next_suggested_task: "Task accepted. Proceed to next goal or task.",
|
|
145
|
+
fail_checks: failChecks,
|
|
146
|
+
warn_checks: warnChecks,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 将 AcceptanceVerdict 映射到 AcceptanceStatus(用于回写 status.json)。
|
|
152
|
+
*/
|
|
153
|
+
export function verdictToStatus(verdict: AcceptanceVerdict): AcceptanceStatus {
|
|
154
|
+
switch (verdict) {
|
|
155
|
+
case "ACCEPTED": return "accepted";
|
|
156
|
+
case "REJECTED": return "rejected";
|
|
157
|
+
case "NEEDS_FIX": return "needs_fix";
|
|
158
|
+
case "BLOCKED_BY_APPROVAL": return "blocked";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.7.2: ACCEPTANCE.md 模板生成 — 人类可读的验收报告导出。
|
|
3
|
+
*
|
|
4
|
+
* 将 AcceptanceResult 渲染为 Markdown,落盘到 .patchwarden/tasks/{task_id}/ACCEPTANCE.md。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AcceptanceResult, AcceptanceEvidence } from "./acceptanceEngine.js";
|
|
8
|
+
import { PATCHWARDEN_VERSION } from "../version.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 生成 ACCEPTANCE.md 内容。
|
|
12
|
+
*/
|
|
13
|
+
export function renderAcceptanceMarkdown(
|
|
14
|
+
taskId: string,
|
|
15
|
+
result: AcceptanceResult,
|
|
16
|
+
evidence: AcceptanceEvidence
|
|
17
|
+
): string {
|
|
18
|
+
const lines: string[] = [];
|
|
19
|
+
const now = new Date().toISOString();
|
|
20
|
+
|
|
21
|
+
lines.push("# Acceptance Report — " + taskId);
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push("Generated: " + now);
|
|
24
|
+
lines.push("");
|
|
25
|
+
|
|
26
|
+
// ── Verdict ──────────────────────────────────────────────
|
|
27
|
+
lines.push("## Verdict");
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push("| Field | Value |");
|
|
30
|
+
lines.push("| --- | --- |");
|
|
31
|
+
lines.push("| Verdict | **" + result.verdict + "** |");
|
|
32
|
+
lines.push("| Acceptance Status | " + (result.acceptance_status ?? "null") + " |");
|
|
33
|
+
lines.push("| Reason | " + result.reason + " |");
|
|
34
|
+
lines.push("| Next Suggested Task | " + result.next_suggested_task + " |");
|
|
35
|
+
lines.push("");
|
|
36
|
+
|
|
37
|
+
// ── Evidence Summary ────────────────────────────────────
|
|
38
|
+
lines.push("## Evidence Summary");
|
|
39
|
+
lines.push("");
|
|
40
|
+
lines.push("| Evidence | Present |");
|
|
41
|
+
lines.push("| --- | --- |");
|
|
42
|
+
lines.push("| result.md | " + (evidence.result_md_exists ? "YES" : "NO") + " |");
|
|
43
|
+
lines.push("| result.json | " + (evidence.result_json_exists ? "YES" : "NO") + " |");
|
|
44
|
+
lines.push("| verify.json | " + (evidence.verify_json_exists ? "YES" : "NO") + " |");
|
|
45
|
+
lines.push("| test.log | " + (evidence.test_log_exists ? "YES" : "NO") + " |");
|
|
46
|
+
lines.push("| git.diff | " + (evidence.git_diff_exists ? "YES" : "NO") + " |");
|
|
47
|
+
lines.push("| verify_status | " + (evidence.verify_status ?? "N/A") + " |");
|
|
48
|
+
lines.push("| artifact_status | " + (evidence.artifact_status ?? "N/A") + " |");
|
|
49
|
+
lines.push("| out_of_scope_changes | " + evidence.new_out_of_scope_changes.length + " |");
|
|
50
|
+
lines.push("| release_claims_unverified | " + (evidence.release_claims_unverified ? "YES" : "NO") + " |");
|
|
51
|
+
lines.push("");
|
|
52
|
+
|
|
53
|
+
// ── Task Metadata ────────────────────────────────────────
|
|
54
|
+
if (evidence.goal || evidence.scope || evidence.forbidden || evidence.verification || evidence.done_evidence) {
|
|
55
|
+
lines.push("## Task Acceptance Criteria");
|
|
56
|
+
lines.push("");
|
|
57
|
+
if (evidence.goal) {
|
|
58
|
+
lines.push("- **Goal**: " + evidence.goal);
|
|
59
|
+
}
|
|
60
|
+
if (evidence.scope && evidence.scope.length > 0) {
|
|
61
|
+
lines.push("- **Scope**: " + evidence.scope.join(", "));
|
|
62
|
+
}
|
|
63
|
+
if (evidence.forbidden && evidence.forbidden.length > 0) {
|
|
64
|
+
lines.push("- **Forbidden**: " + evidence.forbidden.join(", "));
|
|
65
|
+
}
|
|
66
|
+
if (evidence.verification && evidence.verification.length > 0) {
|
|
67
|
+
lines.push("- **Verification**: " + evidence.verification.join(", "));
|
|
68
|
+
}
|
|
69
|
+
if (evidence.done_evidence && evidence.done_evidence.length > 0) {
|
|
70
|
+
lines.push("- **Done Evidence**: " + evidence.done_evidence.join(", "));
|
|
71
|
+
}
|
|
72
|
+
lines.push("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Fail Checks ─────────────────────────────────────────
|
|
76
|
+
if (result.fail_checks.length > 0) {
|
|
77
|
+
lines.push("## Fail-Level Checks");
|
|
78
|
+
lines.push("");
|
|
79
|
+
for (const check of result.fail_checks) {
|
|
80
|
+
lines.push("- **" + check.name + "**: " + check.detail);
|
|
81
|
+
}
|
|
82
|
+
lines.push("");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Warn Checks ─────────────────────────────────────────
|
|
86
|
+
if (result.warn_checks.length > 0) {
|
|
87
|
+
lines.push("## Warn-Level Checks");
|
|
88
|
+
lines.push("");
|
|
89
|
+
for (const check of result.warn_checks) {
|
|
90
|
+
lines.push("- **" + check.name + "**: " + check.detail);
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Reasons ─────────────────────────────────────────────
|
|
96
|
+
if (result.reasons.length > 0) {
|
|
97
|
+
lines.push("## Reasons");
|
|
98
|
+
lines.push("");
|
|
99
|
+
for (const reason of result.reasons) {
|
|
100
|
+
lines.push("- " + reason);
|
|
101
|
+
}
|
|
102
|
+
lines.push("");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Required Evidence ────────────────────────────────────
|
|
106
|
+
if (result.required_evidence.length > 0) {
|
|
107
|
+
lines.push("## Required Evidence");
|
|
108
|
+
lines.push("");
|
|
109
|
+
for (const reqEvidence of result.required_evidence) {
|
|
110
|
+
lines.push("- " + reqEvidence);
|
|
111
|
+
}
|
|
112
|
+
lines.push("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lines.push("---");
|
|
116
|
+
lines.push("");
|
|
117
|
+
lines.push("This report was generated by PatchWarden v" + PATCHWARDEN_VERSION + " acceptanceEngine.");
|
|
118
|
+
lines.push("");
|
|
119
|
+
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.8.0: Goal Session 依赖图 — 就绪/阻塞判定、下一子目标建议、环检测、拓扑排序。
|
|
3
|
+
*
|
|
4
|
+
* 所有函数均为纯函数,不修改传入的 goalStatus 对象。
|
|
5
|
+
* 依赖语义:subgoal.depends_on 引用其他 subgoal 的 id,被引用者 status 必须为
|
|
6
|
+
* "accepted" 才算依赖满足。引用不存在的 id 视为数据不一致,按阻塞处理。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { GoalStatus, Subgoal } from "./goalStatus.js";
|
|
10
|
+
import { PatchWardenError } from "../errors.js";
|
|
11
|
+
|
|
12
|
+
// ── 内部工具 ──────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 构建 subgoal_id → Subgoal 的查找表。
|
|
16
|
+
*/
|
|
17
|
+
function buildSubgoalMap(goalStatus: GoalStatus): Map<string, Subgoal> {
|
|
18
|
+
const map = new Map<string, Subgoal>();
|
|
19
|
+
for (const s of goalStatus.subgoals) {
|
|
20
|
+
map.set(s.id, s);
|
|
21
|
+
}
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 判断单个 subgoal 的依赖是否全部满足(所有 depends_on 引用存在且 status 为 accepted)。
|
|
27
|
+
* depends_on 为空视为依赖满足。引用不存在的 id 视为未满足。
|
|
28
|
+
*/
|
|
29
|
+
function areDependenciesMet(subgoal: Subgoal, subgoalMap: Map<string, Subgoal>): boolean {
|
|
30
|
+
for (const depId of subgoal.depends_on) {
|
|
31
|
+
const dep = subgoalMap.get(depId);
|
|
32
|
+
if (!dep) return false;
|
|
33
|
+
if (dep.status !== "accepted") return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── 公共 API ──────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 返回所有 status 为 "ready" 且依赖全部满足的子目标。
|
|
42
|
+
* 依赖满足 = depends_on 中所有 id 都存在且对应 subgoal status 为 "accepted"。
|
|
43
|
+
* depends_on 为空数组视为依赖满足。
|
|
44
|
+
* depends_on 引用不存在的 subgoal 时,该子目标不算 ready(跳过)。
|
|
45
|
+
*/
|
|
46
|
+
export function getReadySubgoals(goalStatus: GoalStatus): Subgoal[] {
|
|
47
|
+
const subgoalMap = buildSubgoalMap(goalStatus);
|
|
48
|
+
return goalStatus.subgoals.filter(
|
|
49
|
+
(s) => s.status === "ready" && areDependenciesMet(s, subgoalMap)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 返回所有 status 为 "ready" 但依赖未全部满足的子目标,附带阻塞它的依赖 id 列表。
|
|
55
|
+
* blocked_by 包含 depends_on 中 status 不为 "accepted" 的 subgoal_id,
|
|
56
|
+
* 以及引用不存在的 id。
|
|
57
|
+
*/
|
|
58
|
+
export function getBlockedSubgoals(
|
|
59
|
+
goalStatus: GoalStatus
|
|
60
|
+
): Array<{ subgoal: Subgoal; blocked_by: string[] }> {
|
|
61
|
+
const subgoalMap = buildSubgoalMap(goalStatus);
|
|
62
|
+
const result: Array<{ subgoal: Subgoal; blocked_by: string[] }> = [];
|
|
63
|
+
|
|
64
|
+
for (const s of goalStatus.subgoals) {
|
|
65
|
+
if (s.status !== "ready") continue;
|
|
66
|
+
const blockedBy: string[] = [];
|
|
67
|
+
for (const depId of s.depends_on) {
|
|
68
|
+
const dep = subgoalMap.get(depId);
|
|
69
|
+
if (!dep) {
|
|
70
|
+
blockedBy.push(depId);
|
|
71
|
+
} else if (dep.status !== "accepted") {
|
|
72
|
+
blockedBy.push(depId);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (blockedBy.length > 0) {
|
|
76
|
+
result.push({ subgoal: s, blocked_by: blockedBy });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 建议下一个可执行的子目标。
|
|
85
|
+
*
|
|
86
|
+
* 优先级:
|
|
87
|
+
* 1. 有 ready 子目标 → 返回第一个(按 subgoal 添加顺序)的 { subgoal_id, title, depends_on }
|
|
88
|
+
* 2. 无 ready 但有 blocked → 返回 { subgoal_id: null, reason: "dependencies_not_met", blocked_by: [...] }
|
|
89
|
+
* 3. 无 ready 无 blocked → 返回 { subgoal_id: null, reason: "no_ready_subgoal" }
|
|
90
|
+
*/
|
|
91
|
+
export function suggestNextSubgoal(goalStatus: GoalStatus): {
|
|
92
|
+
subgoal_id: string | null;
|
|
93
|
+
title?: string;
|
|
94
|
+
depends_on?: string[];
|
|
95
|
+
reason?: string;
|
|
96
|
+
blocked_by?: string[];
|
|
97
|
+
} {
|
|
98
|
+
const ready = getReadySubgoals(goalStatus);
|
|
99
|
+
if (ready.length > 0) {
|
|
100
|
+
const first = ready[0];
|
|
101
|
+
return {
|
|
102
|
+
subgoal_id: first.id,
|
|
103
|
+
title: first.title,
|
|
104
|
+
depends_on: [...first.depends_on],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const blocked = getBlockedSubgoals(goalStatus);
|
|
109
|
+
if (blocked.length > 0) {
|
|
110
|
+
return {
|
|
111
|
+
subgoal_id: null,
|
|
112
|
+
reason: "dependencies_not_met",
|
|
113
|
+
blocked_by: blocked.map((b) => b.subgoal.id),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
subgoal_id: null,
|
|
119
|
+
reason: "no_ready_subgoal",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 检测 subgoal 依赖图是否有环(DFS 三色标记法)。
|
|
125
|
+
*
|
|
126
|
+
* 沿 depends_on 深度优先遍历,遇到当前路径上正在访问的节点即为环。
|
|
127
|
+
* depends_on 引用不存在的 subgoal 不算环(忽略)。
|
|
128
|
+
*
|
|
129
|
+
* @returns 有环时返回环上的 subgoal_id 数组(任一环即可);无环返回 null。
|
|
130
|
+
*/
|
|
131
|
+
export function detectCycle(goalStatus: GoalStatus): string[] | null {
|
|
132
|
+
const subgoalMap = buildSubgoalMap(goalStatus);
|
|
133
|
+
|
|
134
|
+
const WHITE = 0;
|
|
135
|
+
const GRAY = 1;
|
|
136
|
+
const BLACK = 2;
|
|
137
|
+
const color = new Map<string, number>();
|
|
138
|
+
for (const s of goalStatus.subgoals) {
|
|
139
|
+
color.set(s.id, WHITE);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let cycle: string[] | null = null;
|
|
143
|
+
|
|
144
|
+
const dfs = (id: string, path: string[]): boolean => {
|
|
145
|
+
color.set(id, GRAY);
|
|
146
|
+
path.push(id);
|
|
147
|
+
|
|
148
|
+
const node = subgoalMap.get(id);
|
|
149
|
+
if (node) {
|
|
150
|
+
for (const depId of node.depends_on) {
|
|
151
|
+
if (!subgoalMap.has(depId)) continue; // 引用不存在,忽略
|
|
152
|
+
const depColor = color.get(depId);
|
|
153
|
+
if (depColor === GRAY) {
|
|
154
|
+
// 发现环:截取从依赖节点到当前节点的路径
|
|
155
|
+
const cycleStart = path.indexOf(depId);
|
|
156
|
+
cycle = path.slice(cycleStart);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (depColor === WHITE) {
|
|
160
|
+
if (dfs(depId, path)) return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
path.pop();
|
|
166
|
+
color.set(id, BLACK);
|
|
167
|
+
return false;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const s of goalStatus.subgoals) {
|
|
171
|
+
if (color.get(s.id) === WHITE) {
|
|
172
|
+
if (dfs(s.id, [])) return cycle;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 对 subgoal 做拓扑排序(依赖在前)。
|
|
181
|
+
*
|
|
182
|
+
* 如果 B depends_on A,则 A 排在 B 之前。
|
|
183
|
+
* depends_on 引用不存在的 id 会被忽略(不影响排序)。
|
|
184
|
+
*
|
|
185
|
+
* @throws {PatchWardenError} reason="dependency_cycle" 当依赖图存在环时。
|
|
186
|
+
* @returns subgoal_id 数组,依赖项在前。
|
|
187
|
+
*/
|
|
188
|
+
export function topologicalSort(goalStatus: GoalStatus): string[] {
|
|
189
|
+
const cycle = detectCycle(goalStatus);
|
|
190
|
+
if (cycle) {
|
|
191
|
+
throw new PatchWardenError(
|
|
192
|
+
"dependency_cycle",
|
|
193
|
+
`Dependency cycle detected among subgoals: ${cycle.join(" -> ")}`,
|
|
194
|
+
"Remove the circular dependency among subgoals before performing topological sort.",
|
|
195
|
+
true,
|
|
196
|
+
{ cycle }
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const subgoalMap = buildSubgoalMap(goalStatus);
|
|
201
|
+
const visited = new Set<string>();
|
|
202
|
+
const result: string[] = [];
|
|
203
|
+
|
|
204
|
+
const visit = (id: string): void => {
|
|
205
|
+
if (visited.has(id)) return;
|
|
206
|
+
visited.add(id);
|
|
207
|
+
|
|
208
|
+
const node = subgoalMap.get(id);
|
|
209
|
+
if (node) {
|
|
210
|
+
for (const depId of node.depends_on) {
|
|
211
|
+
if (subgoalMap.has(depId)) {
|
|
212
|
+
visit(depId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
result.push(id);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
for (const s of goalStatus.subgoals) {
|
|
221
|
+
visit(s.id);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
}
|