add-coder 0.3.26 → 0.3.28
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 +282 -155
- package/dist/index.js +481 -209
- package/package.json +15 -4
- package/templates/.add-coder-src-hash.json +186 -185
- package/templates/adapters/claude/hooks/doc-format-guard.ts +32 -0
- package/templates/adapters/claude/hooks/lib/claude-env.ts +7 -0
- package/templates/adapters/claude/hooks/notification.ts +24 -0
- package/templates/adapters/claude/hooks/permission-denied.ts +33 -0
- package/templates/adapters/claude/hooks/permission-gate.ts +12 -0
- package/templates/adapters/claude/hooks/post-tool-failure.ts +4 -0
- package/templates/adapters/claude/hooks/post-tool-use.ts +17 -0
- package/templates/adapters/claude/hooks/pre-compact.ts +10 -0
- package/templates/adapters/claude/hooks/pre-tool-use.ts +81 -0
- package/templates/adapters/claude/hooks/prompt-submit.ts +82 -0
- package/templates/adapters/claude/hooks/review-checklist.ts +4 -0
- package/templates/adapters/claude/hooks/session-end.ts +10 -0
- package/templates/adapters/claude/hooks/session-start.ts +24 -0
- package/templates/adapters/claude/hooks/stop-check.ts +15 -0
- package/templates/adapters/claude/hooks/stop-failure.ts +34 -0
- package/templates/adapters/claude/hooks/subagent-guard.ts +25 -0
- package/templates/adapters/claude/hooks/subagent-stop.ts +12 -0
- package/templates/adapters/claude/settings.json +13 -13
- package/templates/adapters/codex/config.toml.example +12 -0
- package/templates/adapters/codex/hooks/doc-format-guard.ts +30 -0
- package/templates/adapters/codex/hooks/notification.ts +26 -0
- package/templates/adapters/codex/hooks/permission-gate.ts +15 -0
- package/templates/adapters/codex/hooks/post-tool-failure.ts +14 -0
- package/templates/adapters/codex/hooks/post-tool-use.ts +85 -0
- package/templates/adapters/codex/hooks/pre-compact.ts +20 -0
- package/templates/adapters/codex/hooks/pre-tool-use.ts +141 -0
- package/templates/adapters/codex/hooks/prompt-submit.ts +38 -0
- package/templates/adapters/codex/hooks/review-checklist.ts +5 -0
- package/templates/adapters/codex/hooks/session-end.ts +19 -0
- package/templates/adapters/codex/hooks/session-start.ts +45 -0
- package/templates/adapters/codex/hooks/stop-check.ts +59 -0
- package/templates/adapters/codex/hooks/subagent-guard.ts +22 -0
- package/templates/adapters/codex/hooks/subagent-stop.ts +20 -0
- package/templates/adapters/codex/hooks.json +67 -1
- package/templates/adapters/qoder/hooks/doc-format-guard.ts +34 -0
- package/templates/adapters/qoder/hooks/lib/context-inject.ts +61 -0
- package/templates/adapters/qoder/hooks/lib/qoder-env.ts +23 -0
- package/templates/adapters/qoder/hooks/lib/review-checklist.ts +5 -0
- package/templates/adapters/qoder/hooks/notification.ts +34 -0
- package/templates/adapters/qoder/hooks/permission-gate.ts +26 -0
- package/templates/adapters/qoder/hooks/post-tool-failure.ts +30 -0
- package/templates/adapters/qoder/hooks/post-tool-use.ts +32 -0
- package/templates/adapters/qoder/hooks/pre-compact.ts +16 -0
- package/templates/adapters/qoder/hooks/pre-tool-use.ts +112 -0
- package/templates/adapters/qoder/hooks/prompt-submit.ts +66 -0
- package/templates/adapters/qoder/hooks/review-checklist.ts +24 -0
- package/templates/adapters/qoder/hooks/session-end.ts +27 -0
- package/templates/adapters/qoder/hooks/session-start.ts +83 -0
- package/templates/adapters/qoder/hooks/stop-check.ts +69 -0
- package/templates/adapters/qoder/hooks/subagent-guard.ts +16 -0
- package/templates/adapters/qoder/hooks/subagent-stop.ts +33 -0
- package/templates/adapters/qoder/settings.json +13 -13
- package/templates/adapters/trae/hooks/doc-format-guard.ts +30 -0
- package/templates/adapters/trae/hooks/notification.ts +22 -0
- package/templates/adapters/trae/hooks/permission-gate.ts +15 -0
- package/templates/adapters/trae/hooks/post-tool-failure.ts +14 -0
- package/templates/adapters/trae/hooks/post-tool-use.ts +24 -0
- package/templates/adapters/trae/hooks/pre-compact.ts +15 -0
- package/templates/adapters/trae/hooks/pre-tool-use.ts +40 -0
- package/templates/adapters/trae/hooks/prompt-submit.ts +29 -0
- package/templates/adapters/trae/hooks/review-checklist.ts +5 -0
- package/templates/adapters/trae/hooks/session-end.ts +16 -0
- package/templates/adapters/trae/hooks/session-start.ts +15 -0
- package/templates/adapters/trae/hooks/stop-check.ts +20 -0
- package/templates/adapters/trae/hooks/subagent-guard.ts +22 -0
- package/templates/adapters/trae/hooks/subagent-stop.ts +15 -0
- package/templates/adapters/trae/hooks.json +1 -1
- package/templates/adapters/vscode/hooks/doc-format-guard.ts +27 -0
- package/templates/adapters/vscode/hooks/notification.ts +22 -0
- package/templates/adapters/vscode/hooks/permission-gate.ts +15 -0
- package/templates/adapters/vscode/hooks/post-tool-failure.ts +32 -0
- package/templates/adapters/vscode/hooks/post-tool-use.ts +25 -0
- package/templates/adapters/vscode/hooks/pre-compact.ts +15 -0
- package/templates/adapters/vscode/hooks/pre-tool-use.ts +78 -0
- package/templates/adapters/vscode/hooks/prompt-submit.ts +74 -0
- package/templates/adapters/vscode/hooks/review-checklist.ts +5 -0
- package/templates/adapters/vscode/hooks/session-end.ts +16 -0
- package/templates/adapters/vscode/hooks/session-start.ts +24 -0
- package/templates/adapters/vscode/hooks/stop-check.ts +21 -0
- package/templates/adapters/vscode/hooks/subagent-guard.ts +21 -0
- package/templates/adapters/vscode/hooks/subagent-stop.ts +15 -0
- package/templates/core/docs/ADD-governance-claude-code.md +4 -4
- package/templates/core/docs/ADD-governance-codex.md +6 -6
- package/templates/core/docs/ADD-governance-qoder-cn.md +5 -5
- package/templates/core/docs/ADD-governance-trae.md +3 -3
- package/templates/core/docs/ADD-governance-vscode-copilot.md +22 -23
- package/templates/core/governance/audit-bridge.ts +47 -0
- package/templates/core/governance/common.ts +299 -0
- package/templates/core/governance/context-inject.ts +53 -0
- package/templates/core/governance/doc-format-guard.ts +440 -0
- package/templates/core/governance/notification-router.ts +62 -0
- package/templates/core/governance/notify.ts +66 -0
- package/templates/core/governance/permission-gate-router.ts +42 -0
- package/templates/core/governance/post-tool-failure-router.ts +56 -0
- package/templates/core/governance/post-tool-router.ts +212 -0
- package/templates/core/governance/pre-compact-guard.ts +41 -0
- package/templates/core/governance/pre-tool-guard.ts +309 -0
- package/templates/core/governance/preload-templates.ts +241 -0
- package/templates/core/governance/prompt-router.ts +222 -0
- package/templates/core/governance/review-checklist-guard.ts +186 -0
- package/templates/core/governance/rules.ts +720 -0
- package/templates/core/governance/session-end.ts +88 -0
- package/templates/core/governance/session-start-guard.ts +99 -0
- package/templates/core/governance/state-detect.ts +6 -0
- package/templates/core/governance/stop-router.ts +140 -0
- package/templates/core/governance/subagent-guard-router.ts +35 -0
- package/templates/core/governance/subagent-stop-router.ts +93 -0
- package/templates/core/governance/subagent-stop.ts +75 -0
- package/templates/core/governance/vocabulary.ts +109 -0
- package/templates/core/hooks/doc-format-guard.ts +21 -0
- package/templates/core/hooks/notification.ts +11 -0
- package/templates/core/hooks/permission-gate.ts +7 -0
- package/templates/core/hooks/post-tool-failure.ts +7 -0
- package/templates/core/hooks/post-tool-use.ts +23 -0
- package/templates/core/hooks/pre-compact.ts +9 -0
- package/templates/core/hooks/pre-tool-use.ts +30 -0
- package/templates/core/hooks/prompt-submit.ts +11 -0
- package/templates/core/hooks/review-checklist.ts +4 -0
- package/templates/core/hooks/session-end.ts +10 -0
- package/templates/core/hooks/session-start.ts +9 -0
- package/templates/core/hooks/stop-check.ts +14 -0
- package/templates/core/hooks/subagent-guard.ts +8 -0
- package/templates/core/hooks/subagent-stop.ts +9 -0
- package/templates/core/prisma/add.prisma +140 -82
- package/templates/core/rules/project_rules.md +27 -24
- package/templates/core/scripts/db-ensure.sh +8 -0
- package/templates/core/scripts/gen-plan-index.sh +4 -15
- package/templates/core/scripts/mcp-server/notifications/hitl.ts +9 -1
- package/templates/core/scripts/mcp-server/notifications/hook.ts +25 -9
- package/templates/core/scripts/mcp-server/resources/hitl-approval-widget.ts +34 -0
- package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +14 -2
- package/templates/core/scripts/mcp-server/resources/index.ts +2 -0
- package/templates/core/scripts/mcp-server/sampling/review.ts +25 -10
- package/templates/core/scripts/mcp-server/shared/db-types.ts +33 -8
- package/templates/core/scripts/mcp-server/shared/dps-scoring.strategy.ts +1 -1
- package/templates/core/scripts/mcp-server/shared/env.ts +28 -8
- package/templates/core/scripts/mcp-server/shared/fs.ts +3 -0
- package/templates/core/scripts/mcp-server/shared/hitl-interaction.strategy.ts +2 -3
- package/templates/core/scripts/mcp-server/shared/hitl-lifecycle-mutation.ts +129 -0
- package/templates/core/scripts/mcp-server/shared/hitl-proposal-mutation.ts +98 -0
- package/templates/core/scripts/mcp-server/shared/hitl-ui.ts +3 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-events.ts +62 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-mutation.ts +78 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle-subscriber.ts +145 -0
- package/templates/core/scripts/mcp-server/shared/plan-lifecycle.ts +153 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-events.ts +56 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-mutation.ts +143 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-store.ts +47 -0
- package/templates/core/scripts/mcp-server/shared/plan-round-subscriber.ts +99 -0
- package/templates/core/scripts/mcp-server/shared/plan-status-store.ts +37 -0
- package/templates/core/scripts/mcp-server/shared/plan-tracking-mutation.ts +71 -0
- package/templates/core/scripts/mcp-server/shared/prisma.ts +12 -6
- package/templates/core/scripts/mcp-server/shared/project-root-strategy.ts +1 -2
- package/templates/core/scripts/mcp-server/shared/quant-recheck.strategy.ts +16 -0
- package/templates/core/scripts/mcp-server/shared/redact.ts +28 -0
- package/templates/core/scripts/mcp-server/shared/response.ts +3 -2
- package/templates/core/scripts/mcp-server/shared/runtime-context.ts +77 -0
- package/templates/core/scripts/mcp-server/shared/schema-topology.ts +159 -0
- package/templates/core/scripts/mcp-server/tools/audit.ts +49 -7
- package/templates/core/scripts/mcp-server/tools/context.ts +117 -121
- package/templates/core/scripts/mcp-server/tools/contract.ts +34 -9
- package/templates/core/scripts/mcp-server/tools/gateway/check_add_route_status.ts +50 -3
- package/templates/core/scripts/mcp-server/tools/gateway/check_doc_similarity.ts +218 -0
- package/templates/core/scripts/mcp-server/tools/gateway/check_dps.ts +22 -7
- package/templates/core/scripts/mcp-server/tools/gateway/check_rahs.ts +86 -4
- package/templates/core/scripts/mcp-server/tools/gateway/check_spec_sync.ts +19 -6
- package/templates/core/scripts/mcp-server/tools/gateway/index.ts +2 -0
- package/templates/core/scripts/mcp-server/tools/hitl.ts +160 -75
- package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +9 -2
- package/templates/core/scripts/mcp-server/tools/index.ts +69 -3
- package/templates/core/scripts/mcp-server/tools/plan.ts +152 -17
- package/templates/core/scripts/mcp-server/tools/review.ts +44 -15
- package/templates/core/scripts/mcp-server.ts +60 -1
- package/templates/core/scripts/plan-status-bridge.ts +26 -0
- package/templates/core/skills/add-paradigm/SKILL.md +2 -2
- package/templates/core/templates/add-route-template.schema.json +24 -56
- package/templates/core/templates/collab-contract-template.md +1 -1
- package/templates/core/templates/handoff-multi-round-template.schema.json +48 -0
- package/templates/core/templates/handoff-single-round-template.schema.json +92 -0
- package/templates/core/templates/handoff-single-round.schema.json +1 -1
- package/templates/core/templates/hitl-approval-widget.html +222 -37
- package/templates/core/templates/spec-template.schema.json +9 -46
- package/templates/core/templates/standard-plan-template.schema.json +1 -1
- package/templates/core/templates/tasks-template.schema.json +13 -54
- package/templates/adapters/claude/hooks/doc-format-guard.sh +0 -202
- package/templates/adapters/claude/hooks/lib/common.sh +0 -261
- package/templates/adapters/claude/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/claude/hooks/lib/notify.sh +0 -34
- package/templates/adapters/claude/hooks/lib/preload-templates.sh +0 -187
- package/templates/adapters/claude/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/claude/hooks/lib/state-detect.sh +0 -117
- package/templates/adapters/claude/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/claude/hooks/lib/vocabulary.sh +0 -56
- package/templates/adapters/claude/hooks/notification.sh +0 -45
- package/templates/adapters/claude/hooks/permission-denied.sh +0 -42
- package/templates/adapters/claude/hooks/permission-gate.sh +0 -18
- package/templates/adapters/claude/hooks/post-tool-failure.sh +0 -10
- package/templates/adapters/claude/hooks/post-tool-use.sh +0 -35
- package/templates/adapters/claude/hooks/pre-compact.sh +0 -37
- package/templates/adapters/claude/hooks/pre-tool-use.sh +0 -153
- package/templates/adapters/claude/hooks/prompt-submit.sh +0 -99
- package/templates/adapters/claude/hooks/review-checklist.sh +0 -10
- package/templates/adapters/claude/hooks/session-end.sh +0 -21
- package/templates/adapters/claude/hooks/session-start.sh +0 -36
- package/templates/adapters/claude/hooks/stop-check.sh +0 -45
- package/templates/adapters/claude/hooks/stop-failure.sh +0 -44
- package/templates/adapters/claude/hooks/subagent-guard.sh +0 -36
- package/templates/adapters/claude/hooks/subagent-stop.sh +0 -20
- package/templates/adapters/codex/hooks/doc-format-guard.sh +0 -261
- package/templates/adapters/codex/hooks/lib/common.sh +0 -261
- package/templates/adapters/codex/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/codex/hooks/lib/notify.sh +0 -34
- package/templates/adapters/codex/hooks/lib/preload-templates.sh +0 -187
- package/templates/adapters/codex/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/codex/hooks/lib/state-detect.sh +0 -117
- package/templates/adapters/codex/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/codex/hooks/lib/vocabulary.sh +0 -56
- package/templates/adapters/codex/hooks/notification.sh +0 -45
- package/templates/adapters/codex/hooks/permission-gate.sh +0 -18
- package/templates/adapters/codex/hooks/post-tool-failure.sh +0 -10
- package/templates/adapters/codex/hooks/post-tool-use.sh +0 -138
- package/templates/adapters/codex/hooks/pre-compact.sh +0 -37
- package/templates/adapters/codex/hooks/pre-tool-use.sh +0 -217
- package/templates/adapters/codex/hooks/prompt-submit.sh +0 -87
- package/templates/adapters/codex/hooks/review-checklist.sh +0 -10
- package/templates/adapters/codex/hooks/session-end.sh +0 -38
- package/templates/adapters/codex/hooks/session-start.sh +0 -53
- package/templates/adapters/codex/hooks/stop-check.sh +0 -45
- package/templates/adapters/codex/hooks/subagent-guard.sh +0 -15
- package/templates/adapters/codex/hooks/subagent-stop.sh +0 -47
- package/templates/adapters/qoder/hooks/doc-format-guard.sh +0 -260
- package/templates/adapters/qoder/hooks/lib/common.sh +0 -261
- package/templates/adapters/qoder/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/qoder/hooks/lib/notify.sh +0 -34
- package/templates/adapters/qoder/hooks/lib/preload-templates.sh +0 -187
- package/templates/adapters/qoder/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/qoder/hooks/lib/state-detect.sh +0 -117
- package/templates/adapters/qoder/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/qoder/hooks/lib/vocabulary.sh +0 -56
- package/templates/adapters/qoder/hooks/notification.sh +0 -25
- package/templates/adapters/qoder/hooks/permission-gate.sh +0 -8
- package/templates/adapters/qoder/hooks/post-tool-failure.sh +0 -8
- package/templates/adapters/qoder/hooks/post-tool-use.sh +0 -121
- package/templates/adapters/qoder/hooks/pre-compact.sh +0 -37
- package/templates/adapters/qoder/hooks/pre-tool-use.sh +0 -154
- package/templates/adapters/qoder/hooks/prompt-submit.sh +0 -96
- package/templates/adapters/qoder/hooks/review-checklist.sh +0 -157
- package/templates/adapters/qoder/hooks/session-end.sh +0 -24
- package/templates/adapters/qoder/hooks/session-start.sh +0 -45
- package/templates/adapters/qoder/hooks/stop-check.sh +0 -48
- package/templates/adapters/qoder/hooks/subagent-guard.sh +0 -27
- package/templates/adapters/qoder/hooks/subagent-stop.sh +0 -26
- package/templates/adapters/trae/hooks/doc-format-guard.sh +0 -261
- package/templates/adapters/trae/hooks/lib/common.sh +0 -261
- package/templates/adapters/trae/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/trae/hooks/lib/notify.sh +0 -34
- package/templates/adapters/trae/hooks/lib/preload-templates.sh +0 -187
- package/templates/adapters/trae/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/trae/hooks/lib/state-detect.sh +0 -117
- package/templates/adapters/trae/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/trae/hooks/lib/vocabulary.sh +0 -56
- package/templates/adapters/trae/hooks/notification.sh +0 -45
- package/templates/adapters/trae/hooks/permission-gate.sh +0 -18
- package/templates/adapters/trae/hooks/post-tool-failure.sh +0 -10
- package/templates/adapters/trae/hooks/post-tool-use.sh +0 -138
- package/templates/adapters/trae/hooks/pre-compact.sh +0 -37
- package/templates/adapters/trae/hooks/pre-tool-use.sh +0 -217
- package/templates/adapters/trae/hooks/prompt-submit.sh +0 -87
- package/templates/adapters/trae/hooks/review-checklist.sh +0 -10
- package/templates/adapters/trae/hooks/session-end.sh +0 -38
- package/templates/adapters/trae/hooks/session-start.sh +0 -53
- package/templates/adapters/trae/hooks/stop-check.sh +0 -45
- package/templates/adapters/trae/hooks/subagent-guard.sh +0 -15
- package/templates/adapters/trae/hooks/subagent-stop.sh +0 -47
- package/templates/adapters/vscode/hooks/doc-format-guard.sh +0 -201
- package/templates/adapters/vscode/hooks/lib/common.sh +0 -261
- package/templates/adapters/vscode/hooks/lib/context-inject.sh +0 -96
- package/templates/adapters/vscode/hooks/lib/notify.sh +0 -34
- package/templates/adapters/vscode/hooks/lib/preload-templates.sh +0 -187
- package/templates/adapters/vscode/hooks/lib/session-end.sh +0 -76
- package/templates/adapters/vscode/hooks/lib/state-detect.sh +0 -117
- package/templates/adapters/vscode/hooks/lib/subagent-stop.sh +0 -90
- package/templates/adapters/vscode/hooks/lib/vocabulary.sh +0 -56
- package/templates/adapters/vscode/hooks/notification.sh +0 -45
- package/templates/adapters/vscode/hooks/permission-gate.sh +0 -18
- package/templates/adapters/vscode/hooks/post-tool-failure.sh +0 -28
- package/templates/adapters/vscode/hooks/post-tool-use.sh +0 -35
- package/templates/adapters/vscode/hooks/pre-compact.sh +0 -37
- package/templates/adapters/vscode/hooks/pre-tool-use.sh +0 -153
- package/templates/adapters/vscode/hooks/prompt-submit.sh +0 -99
- package/templates/adapters/vscode/hooks/review-checklist.sh +0 -10
- package/templates/adapters/vscode/hooks/session-end.sh +0 -13
- package/templates/adapters/vscode/hooks/session-start.sh +0 -35
- package/templates/adapters/vscode/hooks/stop-check.sh +0 -53
- package/templates/adapters/vscode/hooks/subagent-guard.sh +0 -37
- package/templates/adapters/vscode/hooks/subagent-stop.sh +0 -12
- package/templates/core/hooks/doc-format-guard.sh +0 -261
- package/templates/core/hooks/lib/common.sh +0 -261
- package/templates/core/hooks/lib/context-inject.sh +0 -96
- package/templates/core/hooks/lib/notify.sh +0 -34
- package/templates/core/hooks/lib/preload-templates.sh +0 -187
- package/templates/core/hooks/lib/session-end.sh +0 -76
- package/templates/core/hooks/lib/state-detect.sh +0 -117
- package/templates/core/hooks/lib/subagent-stop.sh +0 -90
- package/templates/core/hooks/lib/vocabulary.sh +0 -56
- package/templates/core/hooks/notification.sh +0 -45
- package/templates/core/hooks/permission-gate.sh +0 -18
- package/templates/core/hooks/post-tool-failure.sh +0 -10
- package/templates/core/hooks/post-tool-use.sh +0 -138
- package/templates/core/hooks/pre-compact.sh +0 -37
- package/templates/core/hooks/pre-tool-use.sh +0 -217
- package/templates/core/hooks/prompt-submit.sh +0 -87
- package/templates/core/hooks/review-checklist.sh +0 -10
- package/templates/core/hooks/session-end.sh +0 -38
- package/templates/core/hooks/session-start.sh +0 -53
- package/templates/core/hooks/stop-check.sh +0 -45
- package/templates/core/hooks/subagent-guard.sh +0 -15
- package/templates/core/hooks/subagent-stop.sh +0 -47
- package/templates/shared/hooks-lib/common.sh +0 -22
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
2
|
+
import type { PlanLifecycleStatus } from "./plan-lifecycle.js"
|
|
3
|
+
import {
|
|
4
|
+
createPlanLifecycleChangedEnvelope,
|
|
5
|
+
publishPlanLifecycleChanged,
|
|
6
|
+
type PrismaNotifyTransaction,
|
|
7
|
+
} from "./plan-lifecycle-events.js"
|
|
8
|
+
import type { HitlDecisionType } from "./hitl-lifecycle-mutation.js"
|
|
9
|
+
|
|
10
|
+
export interface HitlProposalPlanRow {
|
|
11
|
+
id: string
|
|
12
|
+
projectKey: string
|
|
13
|
+
adapterKey: string
|
|
14
|
+
planName: string
|
|
15
|
+
lifecycle: PlanLifecycleStatus
|
|
16
|
+
revision: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HitlProposalRow {
|
|
20
|
+
id: string
|
|
21
|
+
projectKey: string
|
|
22
|
+
adapterKey: string
|
|
23
|
+
planName: string
|
|
24
|
+
round: number
|
|
25
|
+
type: HitlDecisionType
|
|
26
|
+
status: "DRAFT"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HitlProposalTransaction extends PrismaNotifyTransaction {
|
|
30
|
+
planRecord: {
|
|
31
|
+
findFirst(args: { where: Record<string, unknown> }): Promise<HitlProposalPlanRow | null>
|
|
32
|
+
create(args: { data: Record<string, unknown> }): Promise<HitlProposalPlanRow>
|
|
33
|
+
update(args: { where: Record<string, unknown>; data: Record<string, unknown> }): Promise<HitlProposalPlanRow>
|
|
34
|
+
}
|
|
35
|
+
hitlRecord: {
|
|
36
|
+
findFirst(args: { where: Record<string, unknown>; orderBy: Record<string, unknown> }): Promise<HitlProposalRow | null>
|
|
37
|
+
create(args: { data: Record<string, unknown> }): Promise<HitlProposalRow>
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HitlProposalDatabase {
|
|
42
|
+
$transaction<T>(work: (tx: HitlProposalTransaction) => Promise<T>): Promise<T>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Advisory xact lock makes max(round)+1 atomic per scoped plan/type. */
|
|
46
|
+
export async function createHitlProposalAndPublish(
|
|
47
|
+
database: HitlProposalDatabase,
|
|
48
|
+
input: {
|
|
49
|
+
context: RuntimeContextKey
|
|
50
|
+
planName: string
|
|
51
|
+
planPath: string
|
|
52
|
+
planKeyword: string
|
|
53
|
+
type: HitlDecisionType
|
|
54
|
+
},
|
|
55
|
+
): Promise<{ planProvisioned: boolean; plan: HitlProposalPlanRow; hitl: HitlProposalRow }> {
|
|
56
|
+
return database.$transaction(async (tx) => {
|
|
57
|
+
const lockKey = `${input.context.contextId}:${input.planName}:${input.type}`
|
|
58
|
+
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${lockKey}))`
|
|
59
|
+
const scope = {
|
|
60
|
+
projectKey: input.context.projectKey,
|
|
61
|
+
adapterKey: input.context.adapterKey,
|
|
62
|
+
planName: input.planName,
|
|
63
|
+
}
|
|
64
|
+
const existing = await tx.planRecord.findFirst({ where: scope })
|
|
65
|
+
const plan = existing
|
|
66
|
+
? await tx.planRecord.update({
|
|
67
|
+
where: { id: existing.id, revision: existing.revision },
|
|
68
|
+
data: { revision: { increment: 1 } },
|
|
69
|
+
})
|
|
70
|
+
: await tx.planRecord.create({
|
|
71
|
+
data: {
|
|
72
|
+
...scope,
|
|
73
|
+
planPath: input.planPath,
|
|
74
|
+
planKeyword: input.planKeyword,
|
|
75
|
+
lifecycle: "DRAFT",
|
|
76
|
+
revision: 0,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
const latest = await tx.hitlRecord.findFirst({
|
|
80
|
+
where: { ...scope, type: input.type },
|
|
81
|
+
orderBy: { round: "desc" },
|
|
82
|
+
})
|
|
83
|
+
const hitl = await tx.hitlRecord.create({
|
|
84
|
+
data: {
|
|
85
|
+
...scope,
|
|
86
|
+
type: input.type,
|
|
87
|
+
round: (latest?.round ?? 0) + 1,
|
|
88
|
+
status: "DRAFT",
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
await publishPlanLifecycleChanged(tx, createPlanLifecycleChangedEnvelope({
|
|
92
|
+
context: input.context,
|
|
93
|
+
planId: plan.id,
|
|
94
|
+
revision: plan.revision,
|
|
95
|
+
}))
|
|
96
|
+
return { planProvisioned: existing === null, plan, hitl }
|
|
97
|
+
})
|
|
98
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { randomUUID } from "crypto"
|
|
2
|
+
import * as z from "zod/v4"
|
|
3
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
4
|
+
|
|
5
|
+
export const PLAN_LIFECYCLE_CHANNEL = "add_plan_lifecycle_changed_v1"
|
|
6
|
+
|
|
7
|
+
export const PlanLifecycleChangedEnvelopeSchema = z.object({
|
|
8
|
+
schemaVersion: z.literal(1),
|
|
9
|
+
eventId: z.string().min(1),
|
|
10
|
+
projectKey: z.string().min(1),
|
|
11
|
+
adapterKey: z.string().min(1),
|
|
12
|
+
planId: z.string().min(1),
|
|
13
|
+
revision: z.number().int().nonnegative(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export type PlanLifecycleChangedEnvelope = z.infer<typeof PlanLifecycleChangedEnvelopeSchema>
|
|
17
|
+
|
|
18
|
+
export interface PrismaNotifyTransaction {
|
|
19
|
+
$executeRaw(query: TemplateStringsArray, ...values: unknown[]): Promise<number>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createPlanLifecycleChangedEnvelope(input: {
|
|
23
|
+
context: RuntimeContextKey
|
|
24
|
+
planId: string
|
|
25
|
+
revision: number
|
|
26
|
+
eventId?: string
|
|
27
|
+
}): PlanLifecycleChangedEnvelope {
|
|
28
|
+
return {
|
|
29
|
+
schemaVersion: 1,
|
|
30
|
+
eventId: input.eventId ?? randomUUID(),
|
|
31
|
+
projectKey: input.context.projectKey,
|
|
32
|
+
adapterKey: input.context.adapterKey,
|
|
33
|
+
planId: input.planId,
|
|
34
|
+
revision: input.revision,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parsePlanLifecycleChangedEnvelope(payload: string | undefined): PlanLifecycleChangedEnvelope | null {
|
|
39
|
+
if (!payload) return null
|
|
40
|
+
try {
|
|
41
|
+
const parsed: unknown = JSON.parse(payload)
|
|
42
|
+
const result = PlanLifecycleChangedEnvelopeSchema.safeParse(parsed)
|
|
43
|
+
return result.success ? result.data : null
|
|
44
|
+
} catch {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isLifecycleEventForContext(
|
|
50
|
+
envelope: PlanLifecycleChangedEnvelope,
|
|
51
|
+
context: RuntimeContextKey,
|
|
52
|
+
): boolean {
|
|
53
|
+
return envelope.projectKey === context.projectKey && envelope.adapterKey === context.adapterKey
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function publishPlanLifecycleChanged(
|
|
57
|
+
tx: PrismaNotifyTransaction,
|
|
58
|
+
envelope: PlanLifecycleChangedEnvelope,
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const payload = JSON.stringify(envelope)
|
|
61
|
+
await tx.$executeRaw`SELECT pg_notify(${PLAN_LIFECYCLE_CHANNEL}, ${payload})`
|
|
62
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
2
|
+
import { assertLifecycleTransition, type PlanLifecycleStatus } from "./plan-lifecycle.js"
|
|
3
|
+
import {
|
|
4
|
+
createPlanLifecycleChangedEnvelope,
|
|
5
|
+
publishPlanLifecycleChanged,
|
|
6
|
+
type PrismaNotifyTransaction,
|
|
7
|
+
} from "./plan-lifecycle-events.js"
|
|
8
|
+
|
|
9
|
+
export interface PlanLifecycleMutationRow {
|
|
10
|
+
id: string
|
|
11
|
+
projectKey: string
|
|
12
|
+
adapterKey: string
|
|
13
|
+
planName: string
|
|
14
|
+
lifecycle: PlanLifecycleStatus
|
|
15
|
+
revision: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PlanLifecycleTransaction extends PrismaNotifyTransaction {
|
|
19
|
+
planRecord: {
|
|
20
|
+
findFirst(args: { where: Record<string, unknown> }): Promise<PlanLifecycleMutationRow | null>
|
|
21
|
+
update(args: {
|
|
22
|
+
where: Record<string, unknown>
|
|
23
|
+
data: { lifecycle: PlanLifecycleStatus; revision: { increment: 1 } }
|
|
24
|
+
}): Promise<PlanLifecycleMutationRow>
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PlanLifecycleDatabase {
|
|
29
|
+
$transaction<T>(work: (tx: PlanLifecycleTransaction) => Promise<T>): Promise<T>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TransitionPlanLifecycleInput {
|
|
33
|
+
context: RuntimeContextKey
|
|
34
|
+
planName: string
|
|
35
|
+
to: PlanLifecycleStatus
|
|
36
|
+
expectedRevision?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function transitionPlanLifecycle(
|
|
40
|
+
database: PlanLifecycleDatabase,
|
|
41
|
+
input: TransitionPlanLifecycleInput,
|
|
42
|
+
): Promise<PlanLifecycleMutationRow> {
|
|
43
|
+
return database.$transaction(async (tx) => {
|
|
44
|
+
const current = await tx.planRecord.findFirst({
|
|
45
|
+
where: {
|
|
46
|
+
projectKey: input.context.projectKey,
|
|
47
|
+
adapterKey: input.context.adapterKey,
|
|
48
|
+
planName: input.planName,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
if (!current) {
|
|
52
|
+
throw new Error(`当前 RuntimeContextKey 下不存在 Plan: ${input.planName}`)
|
|
53
|
+
}
|
|
54
|
+
if (current.projectKey !== input.context.projectKey || current.adapterKey !== input.context.adapterKey) {
|
|
55
|
+
throw new Error("拒绝迁移 scope 外 Plan lifecycle")
|
|
56
|
+
}
|
|
57
|
+
if (input.expectedRevision !== undefined && current.revision !== input.expectedRevision) {
|
|
58
|
+
throw new Error(`Plan lifecycle revision 冲突: expected=${input.expectedRevision}, actual=${current.revision}`)
|
|
59
|
+
}
|
|
60
|
+
assertLifecycleTransition(current.lifecycle, input.to)
|
|
61
|
+
if (current.lifecycle === input.to) return current
|
|
62
|
+
|
|
63
|
+
const updated = await tx.planRecord.update({
|
|
64
|
+
where: { id: current.id, revision: current.revision },
|
|
65
|
+
data: { lifecycle: input.to, revision: { increment: 1 } },
|
|
66
|
+
})
|
|
67
|
+
if (updated.revision !== current.revision + 1) {
|
|
68
|
+
throw new Error(`Plan lifecycle revision 未按预期递增: ${current.revision} → ${updated.revision}`)
|
|
69
|
+
}
|
|
70
|
+
const envelope = createPlanLifecycleChangedEnvelope({
|
|
71
|
+
context: input.context,
|
|
72
|
+
planId: updated.id,
|
|
73
|
+
revision: updated.revision,
|
|
74
|
+
})
|
|
75
|
+
await publishPlanLifecycleChanged(tx, envelope)
|
|
76
|
+
return updated
|
|
77
|
+
})
|
|
78
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
2
|
+
import type { PlanStatusResolution } from "./plan-lifecycle.js"
|
|
3
|
+
import {
|
|
4
|
+
isLifecycleEventForContext,
|
|
5
|
+
parsePlanLifecycleChangedEnvelope,
|
|
6
|
+
PLAN_LIFECYCLE_CHANNEL,
|
|
7
|
+
type PlanLifecycleChangedEnvelope,
|
|
8
|
+
} from "./plan-lifecycle-events.js"
|
|
9
|
+
|
|
10
|
+
export interface PgNotification {
|
|
11
|
+
channel: string
|
|
12
|
+
payload?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LifecycleListenClient {
|
|
16
|
+
connect(): Promise<unknown>
|
|
17
|
+
query(sql: string): Promise<unknown>
|
|
18
|
+
end(): Promise<void>
|
|
19
|
+
on(event: "notification", listener: (message: PgNotification) => void): this
|
|
20
|
+
on(event: "error", listener: (error: Error) => void): this
|
|
21
|
+
on(event: "end", listener: () => void): this
|
|
22
|
+
removeAllListeners(): this
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type LifecycleListenClientFactory = () => LifecycleListenClient | Promise<LifecycleListenClient>
|
|
26
|
+
|
|
27
|
+
export interface PlanLifecycleSubscriberOptions {
|
|
28
|
+
context: RuntimeContextKey
|
|
29
|
+
clientFactory: LifecycleListenClientFactory
|
|
30
|
+
resolveStatus(input: {
|
|
31
|
+
context: RuntimeContextKey
|
|
32
|
+
planId?: string
|
|
33
|
+
reason: "initial" | "notification" | "reconnect"
|
|
34
|
+
}): Promise<PlanStatusResolution>
|
|
35
|
+
onSnapshot(snapshot: PlanStatusResolution, envelope?: PlanLifecycleChangedEnvelope): void | Promise<void>
|
|
36
|
+
onError?(error: Error): void
|
|
37
|
+
reconnectDelayMs?: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class PlanLifecycleSubscriber {
|
|
41
|
+
readonly #options: PlanLifecycleSubscriberOptions
|
|
42
|
+
#client: LifecycleListenClient | null = null
|
|
43
|
+
#stopping = false
|
|
44
|
+
#startedOnce = false
|
|
45
|
+
#reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
|
46
|
+
#refreshQueue: Promise<void> = Promise.resolve()
|
|
47
|
+
|
|
48
|
+
constructor(options: PlanLifecycleSubscriberOptions) {
|
|
49
|
+
this.#options = options
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async start(): Promise<void> {
|
|
53
|
+
if (this.#client) return
|
|
54
|
+
this.#stopping = false
|
|
55
|
+
try {
|
|
56
|
+
await this.#connectAndHydrate("initial")
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.#scheduleReconnect()
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async stop(): Promise<void> {
|
|
64
|
+
this.#stopping = true
|
|
65
|
+
if (this.#reconnectTimer) clearTimeout(this.#reconnectTimer)
|
|
66
|
+
this.#reconnectTimer = null
|
|
67
|
+
const client = this.#client
|
|
68
|
+
this.#client = null
|
|
69
|
+
if (!client) return
|
|
70
|
+
client.removeAllListeners()
|
|
71
|
+
try { await client.query(`UNLISTEN ${PLAN_LIFECYCLE_CHANNEL}`) } catch { /* connection may already be gone */ }
|
|
72
|
+
await client.end().catch(() => undefined)
|
|
73
|
+
await this.#refreshQueue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async #connectAndHydrate(reason: "initial" | "reconnect"): Promise<void> {
|
|
77
|
+
const client = await this.#options.clientFactory()
|
|
78
|
+
this.#client = client
|
|
79
|
+
client.on("notification", (message) => this.#handleNotification(message))
|
|
80
|
+
client.on("error", (error) => this.#handleDisconnect(client, error))
|
|
81
|
+
client.on("end", () => this.#handleDisconnect(client, new Error("PostgreSQL LISTEN session ended")))
|
|
82
|
+
try {
|
|
83
|
+
await client.connect()
|
|
84
|
+
await client.query(`LISTEN ${PLAN_LIFECYCLE_CHANNEL}`)
|
|
85
|
+
await this.#refresh(undefined, reason)
|
|
86
|
+
this.#startedOnce = true
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (this.#client === client) this.#client = null
|
|
89
|
+
client.removeAllListeners()
|
|
90
|
+
await client.end().catch(() => undefined)
|
|
91
|
+
const normalized = error instanceof Error ? error : new Error(String(error))
|
|
92
|
+
this.#options.onError?.(normalized)
|
|
93
|
+
throw normalized
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#handleNotification(message: PgNotification): void {
|
|
98
|
+
if (this.#stopping || message.channel !== PLAN_LIFECYCLE_CHANNEL) return
|
|
99
|
+
const envelope = parsePlanLifecycleChangedEnvelope(message.payload)
|
|
100
|
+
if (!envelope || !isLifecycleEventForContext(envelope, this.#options.context)) return
|
|
101
|
+
this.#refreshQueue = this.#refreshQueue
|
|
102
|
+
.then(() => this.#refresh(envelope, "notification"))
|
|
103
|
+
.catch((error: unknown) => this.#options.onError?.(error instanceof Error ? error : new Error(String(error))))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#handleDisconnect(client: LifecycleListenClient, error: Error): void {
|
|
107
|
+
if (this.#stopping || this.#client !== client) return
|
|
108
|
+
this.#client = null
|
|
109
|
+
client.removeAllListeners()
|
|
110
|
+
this.#options.onError?.(error)
|
|
111
|
+
if (this.#reconnectTimer) return
|
|
112
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
113
|
+
this.#reconnectTimer = null
|
|
114
|
+
if (this.#stopping) return
|
|
115
|
+
this.#connectAndHydrate(this.#startedOnce ? "reconnect" : "initial")
|
|
116
|
+
.catch(() => this.#scheduleReconnect())
|
|
117
|
+
}, this.#options.reconnectDelayMs ?? 1000)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#scheduleReconnect(): void {
|
|
121
|
+
if (this.#stopping || this.#reconnectTimer) return
|
|
122
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
123
|
+
this.#reconnectTimer = null
|
|
124
|
+
this.#connectAndHydrate(this.#startedOnce ? "reconnect" : "initial")
|
|
125
|
+
.catch(() => this.#scheduleReconnect())
|
|
126
|
+
}, this.#options.reconnectDelayMs ?? 1000)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async #refresh(
|
|
130
|
+
envelope: PlanLifecycleChangedEnvelope | undefined,
|
|
131
|
+
reason: "initial" | "notification" | "reconnect",
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const snapshot = await this.#options.resolveStatus({
|
|
134
|
+
context: this.#options.context,
|
|
135
|
+
planId: envelope?.planId,
|
|
136
|
+
reason,
|
|
137
|
+
})
|
|
138
|
+
await this.#options.onSnapshot(snapshot, envelope)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function createPgLifecycleListenClient(databaseUrl: string): Promise<LifecycleListenClient> {
|
|
143
|
+
const { Client } = await import("pg")
|
|
144
|
+
return new Client({ connectionString: databaseUrl })
|
|
145
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
2
|
+
|
|
3
|
+
export const PLAN_LIFECYCLE_STATUSES = [
|
|
4
|
+
"DRAFT",
|
|
5
|
+
"ACTIVE",
|
|
6
|
+
"BLOCKED",
|
|
7
|
+
"REJECTED",
|
|
8
|
+
"CLOSED",
|
|
9
|
+
"ABANDONED",
|
|
10
|
+
] as const
|
|
11
|
+
|
|
12
|
+
export type PlanLifecycleStatus = (typeof PLAN_LIFECYCLE_STATUSES)[number]
|
|
13
|
+
export type PlanApprovalStatus = "DRAFT" | "SUBMITTED" | "TONGYI" | "BOHUI"
|
|
14
|
+
|
|
15
|
+
export interface ScopedPlanStatusRow {
|
|
16
|
+
id: string
|
|
17
|
+
projectKey: string
|
|
18
|
+
adapterKey: string
|
|
19
|
+
planName: string
|
|
20
|
+
lifecycle: PlanLifecycleStatus
|
|
21
|
+
revision: number
|
|
22
|
+
doneTasks: number
|
|
23
|
+
totalTasks: number
|
|
24
|
+
checklistTDone: number
|
|
25
|
+
checklistT: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PlanStatusStore {
|
|
29
|
+
findPlan(input: {
|
|
30
|
+
context: RuntimeContextKey
|
|
31
|
+
planName?: string
|
|
32
|
+
planId?: string
|
|
33
|
+
activeOnly?: boolean
|
|
34
|
+
}): Promise<ScopedPlanStatusRow | null>
|
|
35
|
+
findLatestPlanApproval(input: {
|
|
36
|
+
context: RuntimeContextKey
|
|
37
|
+
planName: string
|
|
38
|
+
}): Promise<PlanApprovalStatus | null>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PlanStatusSnapshot {
|
|
42
|
+
availability: "READY"
|
|
43
|
+
source: "database"
|
|
44
|
+
context: Pick<RuntimeContextKey, "projectKey" | "adapterKey" | "contextId">
|
|
45
|
+
planName: string
|
|
46
|
+
planId: string
|
|
47
|
+
lifecycle: PlanLifecycleStatus
|
|
48
|
+
revision: number
|
|
49
|
+
isActive: boolean
|
|
50
|
+
approvalStatus: PlanApprovalStatus | null
|
|
51
|
+
progress: {
|
|
52
|
+
doneTasks: number
|
|
53
|
+
totalTasks: number
|
|
54
|
+
checklistTDone: number
|
|
55
|
+
checklistT: number
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface NoActivePlanSnapshot {
|
|
60
|
+
availability: "READY"
|
|
61
|
+
source: "database"
|
|
62
|
+
context: Pick<RuntimeContextKey, "projectKey" | "adapterKey" | "contextId">
|
|
63
|
+
planName: null
|
|
64
|
+
lifecycle: null
|
|
65
|
+
isActive: false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PlanStatusUnavailable {
|
|
69
|
+
availability: "STATUS_UNAVAILABLE"
|
|
70
|
+
source: "database"
|
|
71
|
+
context: Pick<RuntimeContextKey, "projectKey" | "adapterKey" | "contextId">
|
|
72
|
+
reason: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type PlanStatusResolution = PlanStatusSnapshot | NoActivePlanSnapshot | PlanStatusUnavailable
|
|
76
|
+
|
|
77
|
+
const ALLOWED_TRANSITIONS: Readonly<Record<PlanLifecycleStatus, readonly PlanLifecycleStatus[]>> = {
|
|
78
|
+
DRAFT: ["ACTIVE", "REJECTED", "ABANDONED"],
|
|
79
|
+
ACTIVE: ["BLOCKED", "CLOSED", "ABANDONED"],
|
|
80
|
+
BLOCKED: ["ACTIVE", "CLOSED", "ABANDONED"],
|
|
81
|
+
REJECTED: ["DRAFT", "ACTIVE", "ABANDONED"],
|
|
82
|
+
CLOSED: [],
|
|
83
|
+
ABANDONED: [],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isActiveLifecycle(lifecycle: PlanLifecycleStatus): boolean {
|
|
87
|
+
return lifecycle === "ACTIVE" || lifecycle === "BLOCKED"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function assertLifecycleTransition(from: PlanLifecycleStatus, to: PlanLifecycleStatus): void {
|
|
91
|
+
if (from === to) return
|
|
92
|
+
if (!ALLOWED_TRANSITIONS[from].includes(to)) {
|
|
93
|
+
throw new Error(`非法 Plan lifecycle 迁移: ${from} → ${to}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function snapshotContext(context: RuntimeContextKey) {
|
|
98
|
+
return { projectKey: context.projectKey, adapterKey: context.adapterKey, contextId: context.contextId }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function resolvePlanStatus(
|
|
102
|
+
store: PlanStatusStore,
|
|
103
|
+
context: RuntimeContextKey,
|
|
104
|
+
selector?: string | { planName?: string; planId?: string; activeOnly?: boolean },
|
|
105
|
+
): Promise<PlanStatusResolution> {
|
|
106
|
+
try {
|
|
107
|
+
const selection = typeof selector === "string" ? { planName: selector } : (selector ?? {})
|
|
108
|
+
const plan = await store.findPlan({
|
|
109
|
+
context,
|
|
110
|
+
planName: selection.planName,
|
|
111
|
+
planId: selection.planId,
|
|
112
|
+
activeOnly: selection.activeOnly ?? (selection.planName === undefined && selection.planId === undefined),
|
|
113
|
+
})
|
|
114
|
+
if (!plan) {
|
|
115
|
+
return {
|
|
116
|
+
availability: "READY",
|
|
117
|
+
source: "database",
|
|
118
|
+
context: snapshotContext(context),
|
|
119
|
+
planName: null,
|
|
120
|
+
lifecycle: null,
|
|
121
|
+
isActive: false,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (plan.projectKey !== context.projectKey || plan.adapterKey !== context.adapterKey) {
|
|
125
|
+
throw new Error("PlanStatusStore 返回了 scope 外记录")
|
|
126
|
+
}
|
|
127
|
+
const approvalStatus = await store.findLatestPlanApproval({ context, planName: plan.planName })
|
|
128
|
+
return {
|
|
129
|
+
availability: "READY",
|
|
130
|
+
source: "database",
|
|
131
|
+
context: snapshotContext(context),
|
|
132
|
+
planName: plan.planName,
|
|
133
|
+
planId: plan.id,
|
|
134
|
+
lifecycle: plan.lifecycle,
|
|
135
|
+
revision: plan.revision,
|
|
136
|
+
isActive: isActiveLifecycle(plan.lifecycle),
|
|
137
|
+
approvalStatus,
|
|
138
|
+
progress: {
|
|
139
|
+
doneTasks: plan.doneTasks,
|
|
140
|
+
totalTasks: plan.totalTasks,
|
|
141
|
+
checklistTDone: plan.checklistTDone,
|
|
142
|
+
checklistT: plan.checklistT,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
availability: "STATUS_UNAVAILABLE",
|
|
148
|
+
source: "database",
|
|
149
|
+
context: snapshotContext(context),
|
|
150
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomUUID } from "crypto"
|
|
2
|
+
import * as z from "zod/v4"
|
|
3
|
+
import type { RuntimeContextKey } from "./runtime-context.js"
|
|
4
|
+
import type { PrismaNotifyTransaction } from "./plan-lifecycle-events.js"
|
|
5
|
+
|
|
6
|
+
export const PLAN_ROUND_CHANNEL = "add_plan_round_changed_v1"
|
|
7
|
+
|
|
8
|
+
export const PlanRoundChangedEnvelopeSchema = z.object({
|
|
9
|
+
schemaVersion: z.literal(1),
|
|
10
|
+
eventId: z.string().min(1),
|
|
11
|
+
projectKey: z.string().min(1),
|
|
12
|
+
adapterKey: z.string().min(1),
|
|
13
|
+
planId: z.string().min(1),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export type PlanRoundChangedEnvelope = z.infer<typeof PlanRoundChangedEnvelopeSchema>
|
|
17
|
+
|
|
18
|
+
export function createPlanRoundChangedEnvelope(input: {
|
|
19
|
+
context: RuntimeContextKey
|
|
20
|
+
planId: string
|
|
21
|
+
eventId?: string
|
|
22
|
+
}): PlanRoundChangedEnvelope {
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
eventId: input.eventId ?? randomUUID(),
|
|
26
|
+
projectKey: input.context.projectKey,
|
|
27
|
+
adapterKey: input.context.adapterKey,
|
|
28
|
+
planId: input.planId,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parsePlanRoundChangedEnvelope(payload: string | undefined): PlanRoundChangedEnvelope | null {
|
|
33
|
+
if (!payload) return null
|
|
34
|
+
try {
|
|
35
|
+
const parsed: unknown = JSON.parse(payload)
|
|
36
|
+
const result = PlanRoundChangedEnvelopeSchema.safeParse(parsed)
|
|
37
|
+
return result.success ? result.data : null
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isPlanRoundEventForContext(
|
|
44
|
+
envelope: PlanRoundChangedEnvelope,
|
|
45
|
+
context: RuntimeContextKey,
|
|
46
|
+
): boolean {
|
|
47
|
+
return envelope.projectKey === context.projectKey && envelope.adapterKey === context.adapterKey
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function publishPlanRoundChanged(
|
|
51
|
+
tx: PrismaNotifyTransaction,
|
|
52
|
+
envelope: PlanRoundChangedEnvelope,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
const payload = JSON.stringify(envelope)
|
|
55
|
+
await tx.$executeRaw`SELECT pg_notify(${PLAN_ROUND_CHANNEL}, ${payload})`
|
|
56
|
+
}
|