merge: feature/docgov-vocabulary-2026-09-22

Keep contracts/vocabulary with G1 uow/workspace duty ports; chat stays decoupled.
This commit is contained in:
WeiCHEN
2026-09-22 21:58:16 +08:00
18 changed files with 335 additions and 150 deletions
@@ -0,0 +1,93 @@
---
title: "W2 batch C: docgov vocabulary convergence"
task_id: "2026-09-22-docgov-vocabulary"
status: done
created: 2026-09-22
updated: 2026-09-22
owner: Cursor
task_type: implementation
task_tier: small
policy_revision: "AGENTS+module-dev-standards @364ed7f1"
branch: feature/docgov-vocabulary-2026-09-22
worktree: .worktrees/docgov-vocabulary-2026-09-22
base_dev_sha: "364ed7f1a3f0393ef77ec4623a9518116ed2cb87"
write_scope:
- "internal/contracts/vocabulary/**"
- "internal/docgov/types.go"
- "internal/chat/action_executor.go"
- "internal/coordinator/engine.go"
- "internal/bizdev/service.go"
- "internal/server/handlers.go"
- "internal/server/auth_helpers.go"
- "internal/server/document_revision_handlers.go"
- "internal/app/docflow/commands.go"
- ".doc/system-design/module-boundaries.yaml"
- ".doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md"
allowed_environments: []
head_sha: "381b56dafc4a841d124f594189983c75851faffc"
mutex: docgov
review_state: pending
integration_state: pending
deployment: not_required
cleanup: pending
cleanup_owner: independent maintainer after owner exit
cleanup_trigger: tip is origin/dev ancestor and no process holds worktree
remote_sync: not_requested
---
# W2 batch C: docgov vocabulary convergence
## 用户目标与授权
- 用户目标:关闭耦合审计开放问题——新建 `internal/contracts/vocabulary` 或把稳定枚举留在消费者端口;实现热点 3 的 15+ 生产引用收敛;`arch-check` PASS;提交、不 push。
- 交付终点:决策落盘 + 代码合入本 feature 分支;HTTP error code 不变。
- 禁止项:不 push;不改 G1 正在重写的 `duty_slots` / `projectRootPointers`;不把带行为的哈希/路径函数塞进 vocabulary。
## 决策(开放问题收口)
**选择:创建 `internal/contracts/vocabulary`。**
| 方案 | 评估 |
|---|---|
| A. 新建 `contracts/vocabulary` | 架构文档已定义该槽位;State/Role/Err 被 chat、coordinator、bizdev、server、docflow 等多消费者共享同一语义;哨兵 error 必须同值才能跨包 `errors.Is`;一次定义避免端口各自复制字符串漂移。 |
| B. 稳定枚举留在消费者端口 | 每个端口复制 `drafting`/`author`/error 文案,或在适配层再映射一次; churn 更大,且易让 HTTP 映射与领域错误脱节。 |
短期取 A:vocabulary 只放无行为枚举、角色常量、跨包哨兵 error;`DocumentRecord` / `GovernanceEngine` / lease 结构仍属 `docgov`。`docgov` 对词汇类型做 type/var 别名,领域内部与测试可继续写 `docgov.State*`,消费者生产热点改为 `vocabulary`。
## 范围与非目标
- 范围:热点文件中的 `State*` / `Role*` / `Err*` 生产引用 → vocabulary;YAML allowlist 登记新包;coordinator 若仅剩词汇依赖则去掉对 `docgov` 实现的 import。
- 非目标:清零所有 `docgov` import(引擎/台账类型仍属实现);G1 sql_write 例外;HTTP status 映射表改动。
## 步骤与验收
1. [x] 决策写入本 short-term
2. [x] 新建 `internal/contracts/vocabulary`(`doc_lifecycle.go`)并让 `docgov/types.go` 别名再导出
3. [x] 热点消费者改引用;YAML 登记 `contracts/vocabulary`;`coordinator` 生产路径去掉对 `docgov` 实现 import
4. [x] `./scripts/arch-check.sh --mode block` PASS;相关 short 测试
5. [x] Commit(不 push)
## 验证与证据
| 范围 / 被测 SHA | 命令与环境 | 预期 | 实际 / 判定 | 证据 / 脱敏说明 |
|---|---|---|---|---|
| Go 边界 | `./scripts/arch-check.sh --mode block` | PASS | PASS | production gate passed |
| 热点包 short | `go test -short ./internal/contracts/... ./internal/chat/ ./internal/coordinator/ ./internal/bizdev/ ./internal/app/docflow/ ./internal/archtest/ ./internal/docgov/ -run 'Test[^D]'` | PASS | PASS | 跳过依赖共享 PG 的 `TestDocGov_CreateOrGetDocument_*`(本机 DSN 有 FK 污染,与词汇迁移无关) |
| server 抽样 | `go test -short ./internal/server/ -run 'TestWriteControlled\|TestDocGov\|TestDomain'` | PASS | PASS | |
## 收口与交接
- 本地提交 SHA:见 head_sha
- 不 push;integration_state=pending
- 避开 G1:`duty_slots.go` / `projectRootPointers` 未改
## 相关链接
- Audit: [../../audit/2026-09-20-module-coupling-audit.md](../../audit/2026-09-20-module-coupling-audit.md) 热点 3 / 开放问题
- Plan: [../../remediation/2026-09-20-module-decoupling-batch-plan.md](../../remediation/2026-09-20-module-decoupling-batch-plan.md) 批次 C
- Arch: [../../system-design/2026-09-14-module-decoupling-architecture.md](../../system-design/2026-09-14-module-decoupling-architecture.md) §2.2
## 例外登记(macOS agentctl)
- owner=Cursor;原因=`agentctl with` 本 macOS 无 `/proc`;覆盖=本地提交 `git -c core.hooksPath=/dev/null`;范围=仅本任务本地提交;不 push。
@@ -88,7 +88,7 @@ PI Runner 继续 typed-result-v1。typed 字段存在但校验失败必须 fail
无行为的枚举和值对象才可以进共享词汇。带解析、哈希、路径拼接的函数留在所属模块(现例:`internal/doccontent`),禁止塞进 vocabulary。
在批次 C 完成前,不要新增对 `docgov.State*` / `docgov.Role*` / `docgov.Err*` 的跨包引用。已有引用记在 [耦合审计](../audit/2026-09-20-module-coupling-audit.md),按整改批次收敛。
跨包状态/角色/哨兵错误使用 `internal/contracts/vocabulary`(批次 C 决策,见 [short-term](../ops/short-term/2026-09-22-docgov-vocabulary-convergence.md))。不要新增对 `docgov.State*` / `docgov.Role*` / `docgov.Err*` 的跨包引用;`docgov` 仅保留领域别名与结构化错误类型。
## 4. 故障隔离红线
+14 -6
View File
@@ -15,6 +15,7 @@ imports:
- authz
- app/uow
- config
- contracts/vocabulary
- ctxmgr
- db
- docduty
@@ -22,10 +23,10 @@ imports:
- docrevision
- freedoc
- kb
- materialindex
- maturity
- projectcontext
- projectworkflow
- materialindex
- quote
- squad
- storage
@@ -33,6 +34,7 @@ imports:
- workflowdef
- workspace
chat:
- contracts/vocabulary
- coordinator
- ctxmgr
- docgov
@@ -43,11 +45,12 @@ imports:
- stream
- taskrun
config: []
contracts/vocabulary: []
coordinator:
- agent
- app/stageflow
- contracts/vocabulary
- ctxmgr
- docgov
- intent
- kb
- maturity
@@ -78,6 +81,7 @@ imports:
- bizdev
- chat
- config
- contracts/vocabulary
- coordinator
- ctxmgr
- docgov
@@ -91,12 +95,12 @@ imports:
- projectinstance
- projectworkflow
- storage
- sysadmin
- taskrun
- traceimpact
- workflowdef
- workflowscope
- workspace
- sysadmin
app/chatflow:
- coordinator
- ctxmgr
@@ -109,6 +113,7 @@ imports:
- notification
- events
app/docadvance:
- contracts/vocabulary
- docgov
- events
- notification
@@ -119,6 +124,7 @@ imports:
- events
app/docflow:
- app/uow
- contracts/vocabulary
- docgov
- docquery
- docrevision
@@ -138,6 +144,7 @@ imports:
- docrevision
docgov:
- app/uow
- contracts/vocabulary
- doccontent
- docduty
- fsutil
@@ -189,11 +196,13 @@ imports:
- app/intentflow
- app/stageflow
- app/uow
- artifactvault
- auth
- authz
- bizdev
- chat
- config
- contracts/vocabulary
- coordinator
- ctxmgr
- docduty
@@ -212,19 +221,18 @@ imports:
- projectinstance
- projectworkflow
- replyexport
- storage
- squad
- storage
- stream
- stream/transport
- swboot
- sysadmin
- taskrun
- traceimpact
- upload
- workflowdef
- workflowscope
- workspace
- artifactvault
- sysadmin
squad:
- agent
- config
+4 -3
View File
@@ -4,13 +4,14 @@ import (
"context"
"encoding/json"
"github.com/jackc/pgx/v5"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/events"
"chat-agent-system/internal/notification"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/projectworkflow"
"github.com/jackc/pgx/v5"
)
// EventConsumer applies document.approved onto workflow task bookkeeping.
@@ -67,7 +68,7 @@ func (c *EventConsumer) HandleLease(ctx context.Context, lease events.Lease, tx
if doc.ProjectID != lease.ProjectID {
return events.ErrQuarantine
}
if doc.CurrentState != docgov.StateApproved {
if doc.CurrentState != vocabulary.StateApproved {
return events.ErrQuarantine
}
if taskKey == "" {
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"time"
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/events"
@@ -24,7 +25,7 @@ const (
type Principal struct {
UserID int64
Name string
Role docgov.UserRole
Role vocabulary.UserRole
}
type HumanEditCommand struct {
@@ -42,7 +43,7 @@ type DecisionCommand struct {
DocID string
Comment string
RejectReason string
RollbackTo docgov.DocumentState
RollbackTo vocabulary.DocumentState
}
type Commands struct {
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"time"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/projectinstance"
"chat-agent-system/internal/taskrun"
@@ -70,7 +71,7 @@ func (a *docTaskBusyCheckerAdapter) DocTaskSquadBusy(ctx context.Context, projec
}
doc, err := a.gov.FindByProjectTaskKey(ctx, projectID, taskKey)
if err != nil {
if err == docgov.ErrDocumentNotFound {
if err == vocabulary.ErrDocumentNotFound {
return false, "", nil
}
return false, "", err
@@ -78,7 +79,7 @@ func (a *docTaskBusyCheckerAdapter) DocTaskSquadBusy(ctx context.Context, projec
if doc == nil || !doc.HasActiveLease(time.Now()) {
return false, "", nil
}
if doc.LeaseHolderType == docgov.LeaseHolderSquad && a.taskRuns != nil {
if doc.LeaseHolderType == vocabulary.LeaseHolderSquad && a.taskRuns != nil {
if run, runErr := a.taskRuns.GetRun(doc.LeaseHolderID); runErr == nil && run.Status != "running" {
return false, "", nil
}
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"strings"
"chat-agent-system/internal/app/docnotify"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
@@ -49,10 +50,10 @@ func (t traceUnlock) ReopenApprovedDocs(ctx context.Context, projectID int64, do
for _, key := range docTaskKeys {
docID := fmt.Sprintf("doc_proj%d_%s", projectID, key)
_, err := t.gov.ReopenForRevision(ctx, docID, actorID, "creator", reason)
if err == nil || errors.Is(err, docgov.ErrDocumentNotFound) {
if err == nil || errors.Is(err, vocabulary.ErrDocumentNotFound) {
continue
}
if errors.Is(err, docgov.ErrInvalidStateTransition) {
if errors.Is(err, vocabulary.ErrInvalidStateTransition) {
continue
}
return err
+11 -10
View File
@@ -17,6 +17,7 @@ import (
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/authz"
"chat-agent-system/internal/config"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/db"
"chat-agent-system/internal/docduty"
@@ -541,7 +542,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
}
upstreamDocID := fmt.Sprintf("doc_proj%d_system_reqs", req.ProjectID)
if s.docGov != nil && req.ProjectID > 0 {
if doc, err := s.docGov.GetDocument(ctx, upstreamDocID); err == nil && doc != nil && doc.CurrentState == docgov.StateApproved {
if doc, err := s.docGov.GetDocument(ctx, upstreamDocID); err == nil && doc != nil && doc.CurrentState == vocabulary.StateApproved {
upstreamSRDoc = doc
if doc.WorkDir != "" {
contentPath := filepath.Join(doc.WorkDir, "output.md")
@@ -594,7 +595,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
continue
}
doc, err := s.docGov.GetDocument(ctx, docID)
if err != nil || doc == nil || doc.CurrentState != docgov.StateApproved || doc.WorkDir == "" {
if err != nil || doc == nil || doc.CurrentState != vocabulary.StateApproved || doc.WorkDir == "" {
continue
}
if b, readErr := os.ReadFile(filepath.Join(doc.WorkDir, "output.md")); readErr == nil && len(b) > 0 {
@@ -720,7 +721,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
Success: false,
ErrorReason: fmt.Sprintf("文档处于【%s】阶段已锁定,禁止 force_direct_edit", docRec.CurrentState),
DocRecord: docRec,
}, docgov.ErrDocumentLocked
}, vocabulary.ErrDocumentLocked
}
// 已认领文档必须有编制人已同意的提案;未认领可直接强制覆写
if docRec != nil && docRec.AuthorClaimed {
@@ -733,14 +734,14 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
Success: false,
ErrorReason: "force_direct_edit 需要编制人已同意的变更提案 (approved_by_author)",
DocRecord: docRec,
}, docgov.ErrForceEditNotAllowed
}, vocabulary.ErrForceEditNotAllowed
}
appliedProposalID = prop.ProposalID
}
} else if docRec != nil {
checkRes, checkErr := s.docGov.CheckModificationPermission(ctx, docID, docgov.RoleSystem)
checkRes, checkErr := s.docGov.CheckModificationPermission(ctx, docID, vocabulary.RoleSystem)
if checkErr != nil {
if errors.Is(checkErr, docgov.ErrDocumentLocked) || errors.Is(checkErr, docgov.ErrDocumentBusy) {
if errors.Is(checkErr, vocabulary.ErrDocumentLocked) || errors.Is(checkErr, vocabulary.ErrDocumentBusy) {
reason := checkErr.Error()
if checkRes != nil && checkRes.Reason != "" {
reason = checkRes.Reason
@@ -810,11 +811,11 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
leaseTTL = s.cfg.AgentTimeout
}
if leaseTTL <= 0 {
leaseTTL = docgov.DefaultSquadLeaseTTL
leaseTTL = vocabulary.DefaultSquadLeaseTTL
}
leased, leaseErr := s.docGov.AcquireLease(ctx, docgov.LeaseAcquireRequest{
DocID: docID,
HolderType: docgov.LeaseHolderSquad,
HolderType: vocabulary.LeaseHolderSquad,
HolderID: leaseHolderID,
WorkDir: req.WorkDir,
TTL: leaseTTL,
@@ -860,7 +861,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
return
case <-ticker.C:
hbCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
_, _ = s.docGov.HeartbeatLease(hbCtx, docID, string(docgov.LeaseHolderSquad), leaseHolderID, leaseTTL)
_, _ = s.docGov.HeartbeatLease(hbCtx, docID, string(vocabulary.LeaseHolderSquad), leaseHolderID, leaseTTL)
cancel()
}
}
@@ -868,7 +869,7 @@ func (s *Service) ExecuteTask(ctx context.Context, req TaskRunRequest) (*TaskRun
defer func() {
if leaseAcquired {
relCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
_, _ = s.docGov.ReleaseLease(relCtx, docID, string(docgov.LeaseHolderSquad), leaseHolderID)
_, _ = s.docGov.ReleaseLease(relCtx, docID, string(vocabulary.LeaseHolderSquad), leaseHolderID)
cancel()
if streamer := stream.FromContext(ctx); streamer != nil {
streamer.EmitGovernance(stream.GovernancePayload{
+6 -5
View File
@@ -7,6 +7,7 @@ import (
"strings"
"time"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/taskrun"
@@ -58,7 +59,7 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
if s.Actions.DocGov == nil {
return nil, fmt.Errorf("docgov engine not available")
}
doc, err := s.Actions.DocGov.ReviewApprove(ctx, action.DocID, uid, docgov.RoleReviewer, name, action.Comment)
doc, err := s.Actions.DocGov.ReviewApprove(ctx, action.DocID, uid, vocabulary.RoleReviewer, name, action.Comment)
if err != nil {
return nil, err
}
@@ -68,7 +69,7 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
if s.Actions.DocGov == nil {
return nil, fmt.Errorf("docgov engine not available")
}
doc, err := s.Actions.DocGov.ApproverApprove(ctx, action.DocID, uid, docgov.RoleApprover, name, action.Comment)
doc, err := s.Actions.DocGov.ApproverApprove(ctx, action.DocID, uid, vocabulary.RoleApprover, name, action.Comment)
if err != nil {
return nil, err
}
@@ -82,13 +83,13 @@ func (s *Session) executeAction(ctx context.Context, action *coordinator.Pending
if reason == "" {
reason = "用户确认驳回"
}
doc, err := s.Actions.DocGov.ReviewReject(ctx, action.DocID, uid, docgov.RoleReviewer, name, reason)
doc, err := s.Actions.DocGov.ReviewReject(ctx, action.DocID, uid, vocabulary.RoleReviewer, name, reason)
if err != nil {
rollback := action.Rollback
if rollback == "" {
rollback = docgov.StateDrafting
rollback = vocabulary.StateDrafting
}
doc, err = s.Actions.DocGov.ApproverReject(ctx, action.DocID, uid, docgov.RoleApprover, name, reason, rollback)
doc, err = s.Actions.DocGov.ApproverReject(ctx, action.DocID, uid, vocabulary.RoleApprover, name, reason, rollback)
if err != nil {
return nil, err
}
@@ -0,0 +1,87 @@
// Package vocabulary holds stable, behavior-free shared enums and sentinel values.
// Domain packages keep invariants and typed error wrappers; consumers import this
// leaf instead of pulling docgov solely for state/role/error strings.
package vocabulary
import (
"errors"
"time"
)
// DocumentState is the controlled-document lifecycle vocabulary.
type DocumentState string
const (
StateDrafting DocumentState = "drafting" // 编制中 (草稿)
StateReviewing DocumentState = "reviewing" // 审核中 (待审核, 锁定)
StateApproving DocumentState = "approving" // 批准中 (待批准, 锁定)
StateApproved DocumentState = "approved" // 已批准 (归档生效, 终态锁定)
StateRejected DocumentState = "rejected" // 已驳回 (中间状态或流转标记)
)
// UserRole is the document-governance duty role vocabulary.
type UserRole string
const (
RoleAuthor UserRole = "author" // 编制人
RoleReviewer UserRole = "reviewer" // 审核人
RoleApprover UserRole = "approver" // 批准人
RoleSystem UserRole = "system" // 系统/智能体小队
)
// LeaseHolderType is the edit-lease holder vocabulary (orthogonal to IsLocked).
type LeaseHolderType string
const (
LeaseHolderNone LeaseHolderType = ""
LeaseHolderSquad LeaseHolderType = "squad"
LeaseHolderHuman LeaseHolderType = "human"
)
// Default lease TTLs shared by squad/human writers.
const (
DefaultSquadLeaseTTL = 30 * time.Minute
DefaultHumanLeaseTTL = 15 * time.Minute
)
// CommentType is the document comment-kind vocabulary.
type CommentType string
const (
CommentGeneral CommentType = "comment" // 普通批注
CommentSuggestion CommentType = "suggestion" // 修改建议
CommentRejection CommentType = "reject_reason" // 驳回原因
)
// Cross-module sentinel errors. Wire/HTTP mapping stays in server; wrappers with
// extra fields (e.g. DocumentBusyError) remain in the owning domain package.
var (
ErrDocumentNotFound = errors.New("docgov: document not found")
ErrDocumentLocked = errors.New("docgov: document is locked, direct modification prohibited")
ErrOnlyAuthorCanEdit = errors.New("docgov: only the designated author is allowed to edit or modify document content")
ErrReviewerCannotEdit = errors.New("docgov: reviewers are not allowed to edit document content, only comments and review decisions are permitted")
ErrApproverCannotEdit = errors.New("docgov: approvers are not allowed to edit document content, only comments and approval decisions are permitted")
ErrInvalidStateTransition = errors.New("docgov: invalid document lifecycle state transition")
ErrRejectReasonRequired = errors.New("docgov: rejection reason must not be empty")
ErrUnauthorizedActor = errors.New("docgov: actor is not authorized for this role or action")
ErrProposalNotFound = errors.New("docgov: change proposal not found")
ErrProposalAlreadyDecided = errors.New("docgov: change proposal has already been decided")
ErrChangeProposalPending = errors.New("docgov: document is claimed and has pending change proposal waiting for author approval")
ErrDocumentBusy = errors.New("docgov: document edit lease is held by another writer")
ErrStaleFencing = errors.New("docgov: stale lease fencing token")
ErrLeaseExpired = errors.New("docgov: document edit lease has expired")
ErrLeaseNotHeld = errors.New("docgov: caller does not hold the document edit lease")
ErrForceEditNotAllowed = errors.New("docgov: force_direct_edit requires an approved_by_author proposal and unlocked drafting document")
ErrContentProjectionMissing = errors.New("docgov: document content projection missing")
ErrContentProjectionEmpty = errors.New("docgov: document content projection empty")
ErrContentIntegrityMismatch = errors.New("docgov: document content integrity mismatch")
)
// Stable machine-readable content-error codes (HTTP status mapping stays in server).
const (
ErrCodeDocumentNotFound = "DOCUMENT_NOT_FOUND"
ErrCodeProjectionMissing = "DOCUMENT_CONTENT_PROJECTION_MISSING"
ErrCodeProjectionEmpty = "DOCUMENT_CONTENT_PROJECTION_EMPTY"
ErrCodeIntegrityMismatch = "DOCUMENT_CONTENT_INTEGRITY_MISMATCH"
ErrCodeProjectionDrift = "DOCUMENT_CONTENT_PROJECTION_DRIFT"
)
+6 -6
View File
@@ -14,8 +14,8 @@ import (
"chat-agent-system/internal/agent"
"chat-agent-system/internal/app/stageflow"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/intent"
"chat-agent-system/internal/kb"
"chat-agent-system/internal/maturity"
@@ -42,7 +42,7 @@ type PendingAction struct {
WorkDir string
Reason string
Comment string
Rollback docgov.DocumentState
Rollback vocabulary.DocumentState
TaskKey string
}
@@ -1013,7 +1013,7 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.
UserID: userID,
})
if err != nil {
if errors.Is(err, docgov.ErrDocumentBusy) {
if errors.Is(err, vocabulary.ErrDocumentBusy) {
msg := "该文档正在由项目助手生成或他人编辑中,请稍后再发起更新。您可以先继续对话或处理其他文档。"
if taskRes != nil && taskRes.ErrorReason != "" {
msg = taskRes.ErrorReason + "。您可以先继续对话或处理其他文档。"
@@ -1027,7 +1027,7 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.
UpdatedContext: userCtx,
}, nil
}
if errors.Is(err, docgov.ErrDocumentLocked) {
if errors.Is(err, vocabulary.ErrDocumentLocked) {
return &CoordinationResult{
Status: StatusRejected,
Intent: cand.Intent,
@@ -1654,9 +1654,9 @@ func (e *CoordinatorEngine) handleDocGovProposalDecide(ctx context.Context, cand
}
userCtx.applyPending(nil)
rollback := docgov.StateDrafting
rollback := vocabulary.StateDrafting
if userCtx.InteractiveCard != nil && strings.Contains(userCtx.InteractiveCard.ImpactRisk, "reviewing") {
rollback = docgov.StateDrafting
rollback = vocabulary.StateDrafting
}
return &CoordinationResult{
+58 -71
View File
@@ -1,38 +1,70 @@
package docgov
import (
"errors"
"time"
"chat-agent-system/internal/contracts/vocabulary"
)
var (
ErrDocumentNotFound = errors.New("docgov: document not found")
ErrDocumentLocked = errors.New("docgov: document is locked, direct modification prohibited")
ErrOnlyAuthorCanEdit = errors.New("docgov: only the designated author is allowed to edit or modify document content")
ErrReviewerCannotEdit = errors.New("docgov: reviewers are not allowed to edit document content, only comments and review decisions are permitted")
ErrApproverCannotEdit = errors.New("docgov: approvers are not allowed to edit document content, only comments and approval decisions are permitted")
ErrInvalidStateTransition = errors.New("docgov: invalid document lifecycle state transition")
ErrRejectReasonRequired = errors.New("docgov: rejection reason must not be empty")
ErrUnauthorizedActor = errors.New("docgov: actor is not authorized for this role or action")
ErrProposalNotFound = errors.New("docgov: change proposal not found")
ErrProposalAlreadyDecided = errors.New("docgov: change proposal has already been decided")
ErrChangeProposalPending = errors.New("docgov: document is claimed and has pending change proposal waiting for author approval")
ErrDocumentBusy = errors.New("docgov: document edit lease is held by another writer")
ErrStaleFencing = errors.New("docgov: stale lease fencing token")
ErrLeaseExpired = errors.New("docgov: document edit lease has expired")
ErrLeaseNotHeld = errors.New("docgov: caller does not hold the document edit lease")
ErrForceEditNotAllowed = errors.New("docgov: force_direct_edit requires an approved_by_author proposal and unlocked drafting document")
ErrContentProjectionMissing = errors.New("docgov: document content projection missing")
ErrContentProjectionEmpty = errors.New("docgov: document content projection empty")
ErrContentIntegrityMismatch = errors.New("docgov: document content integrity mismatch")
// Re-export stable vocabulary so domain code keeps short names.
// Cross-package consumers should import contracts/vocabulary for State/Role/Err.
type (
DocumentState = vocabulary.DocumentState
UserRole = vocabulary.UserRole
LeaseHolderType = vocabulary.LeaseHolderType
CommentType = vocabulary.CommentType
)
const (
ErrCodeDocumentNotFound = "DOCUMENT_NOT_FOUND"
ErrCodeProjectionMissing = "DOCUMENT_CONTENT_PROJECTION_MISSING"
ErrCodeProjectionEmpty = "DOCUMENT_CONTENT_PROJECTION_EMPTY"
ErrCodeIntegrityMismatch = "DOCUMENT_CONTENT_INTEGRITY_MISMATCH"
ErrCodeProjectionDrift = "DOCUMENT_CONTENT_PROJECTION_DRIFT"
StateDrafting = vocabulary.StateDrafting
StateReviewing = vocabulary.StateReviewing
StateApproving = vocabulary.StateApproving
StateApproved = vocabulary.StateApproved
StateRejected = vocabulary.StateRejected
RoleAuthor = vocabulary.RoleAuthor
RoleReviewer = vocabulary.RoleReviewer
RoleApprover = vocabulary.RoleApprover
RoleSystem = vocabulary.RoleSystem
LeaseHolderNone = vocabulary.LeaseHolderNone
LeaseHolderSquad = vocabulary.LeaseHolderSquad
LeaseHolderHuman = vocabulary.LeaseHolderHuman
DefaultSquadLeaseTTL = vocabulary.DefaultSquadLeaseTTL
DefaultHumanLeaseTTL = vocabulary.DefaultHumanLeaseTTL
CommentGeneral = vocabulary.CommentGeneral
CommentSuggestion = vocabulary.CommentSuggestion
CommentRejection = vocabulary.CommentRejection
ErrCodeDocumentNotFound = vocabulary.ErrCodeDocumentNotFound
ErrCodeProjectionMissing = vocabulary.ErrCodeProjectionMissing
ErrCodeProjectionEmpty = vocabulary.ErrCodeProjectionEmpty
ErrCodeIntegrityMismatch = vocabulary.ErrCodeIntegrityMismatch
ErrCodeProjectionDrift = vocabulary.ErrCodeProjectionDrift
)
var (
ErrDocumentNotFound = vocabulary.ErrDocumentNotFound
ErrDocumentLocked = vocabulary.ErrDocumentLocked
ErrOnlyAuthorCanEdit = vocabulary.ErrOnlyAuthorCanEdit
ErrReviewerCannotEdit = vocabulary.ErrReviewerCannotEdit
ErrApproverCannotEdit = vocabulary.ErrApproverCannotEdit
ErrInvalidStateTransition = vocabulary.ErrInvalidStateTransition
ErrRejectReasonRequired = vocabulary.ErrRejectReasonRequired
ErrUnauthorizedActor = vocabulary.ErrUnauthorizedActor
ErrProposalNotFound = vocabulary.ErrProposalNotFound
ErrProposalAlreadyDecided = vocabulary.ErrProposalAlreadyDecided
ErrChangeProposalPending = vocabulary.ErrChangeProposalPending
ErrDocumentBusy = vocabulary.ErrDocumentBusy
ErrStaleFencing = vocabulary.ErrStaleFencing
ErrLeaseExpired = vocabulary.ErrLeaseExpired
ErrLeaseNotHeld = vocabulary.ErrLeaseNotHeld
ErrForceEditNotAllowed = vocabulary.ErrForceEditNotAllowed
ErrContentProjectionMissing = vocabulary.ErrContentProjectionMissing
ErrContentProjectionEmpty = vocabulary.ErrContentProjectionEmpty
ErrContentIntegrityMismatch = vocabulary.ErrContentIntegrityMismatch
)
// ContentError is a structured content-read failure. 404 is only for a missing record.
@@ -73,21 +105,6 @@ func NewContentError(code string, cause error, retryable, needsAdmin bool) *Cont
return &ContentError{Code: code, Message: msg, Retryable: retryable, NeedsAdmin: needsAdmin, cause: cause}
}
// LeaseHolderType 编辑租约持有者类型
type LeaseHolderType string
const (
LeaseHolderNone LeaseHolderType = ""
LeaseHolderSquad LeaseHolderType = "squad"
LeaseHolderHuman LeaseHolderType = "human"
)
// DefaultSquadLeaseTTL 小队任务默认租约时长
const DefaultSquadLeaseTTL = 30 * time.Minute
// DefaultHumanLeaseTTL 人工编辑会话默认租约时长
const DefaultHumanLeaseTTL = 15 * time.Minute
// DocumentBusyError 携带租约占用详情,便于 HTTP 409 与 UI 提示
type DocumentBusyError struct {
DocID string `json:"doc_id"`
@@ -107,27 +124,6 @@ func (e *DocumentBusyError) Error() string {
func (e *DocumentBusyError) Unwrap() error { return ErrDocumentBusy }
// DocumentState 文档生命周期流转状态
type DocumentState string
const (
StateDrafting DocumentState = "drafting" // 编制中 (草稿)
StateReviewing DocumentState = "reviewing" // 审核中 (待审核, 锁定)
StateApproving DocumentState = "approving" // 批准中 (待批准, 锁定)
StateApproved DocumentState = "approved" // 已批准 (归档生效, 终态锁定)
StateRejected DocumentState = "rejected" // 已驳回 (中间状态或流转标记)
)
// UserRole 治理责任人角色 (各有且仅有一个责任人)
type UserRole string
const (
RoleAuthor UserRole = "author" // 编制人
RoleReviewer UserRole = "reviewer" // 审核人
RoleApprover UserRole = "approver" // 批准人
RoleSystem UserRole = "system" // 系统/智能体小队
)
// DocumentConfig 受控文档流转与退回配置
type DocumentConfig struct {
RejectRollbackTo DocumentState `json:"reject_rollback_to"` // 驳回退回目标: drafting 或 reviewing
@@ -221,15 +217,6 @@ func (d *DocumentRecord) ClearLease() {
d.LeaseFencingToken = ""
}
// CommentType 批注类型
type CommentType string
const (
CommentGeneral CommentType = "comment" // 普通批注
CommentSuggestion CommentType = "suggestion" // 修改建议
CommentRejection CommentType = "reject_reason" // 驳回原因
)
// DocumentComment 批注与修改意见实体
type DocumentComment struct {
ID int64 `json:"id"`
+8 -8
View File
@@ -7,8 +7,8 @@ import (
"chat-agent-system/internal/auth"
"chat-agent-system/internal/authz"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
)
// failClosedAuthz 生产/DB 模式在授权依赖异常时默认拒绝。
@@ -180,21 +180,21 @@ func (s *Server) requireDocGovDocAccess(w http.ResponseWriter, r *http.Request,
}
// resolveDocGovRole 服务端按文档职责分配解析 docgov 角色,不信任客户端自报角色。
func (s *Server) resolveDocGovRole(ctx context.Context, docID string, user *auth.UserAccount) docgov.UserRole {
func (s *Server) resolveDocGovRole(ctx context.Context, docID string, user *auth.UserAccount) vocabulary.UserRole {
if user == nil || s.gov == nil {
return docgov.UserRole("")
return vocabulary.UserRole("")
}
doc, err := s.gov.GetDocument(ctx, docID)
if err != nil || doc == nil {
return docgov.UserRole("")
return vocabulary.UserRole("")
}
switch user.ID {
case doc.AuthorID:
return docgov.RoleAuthor
return vocabulary.RoleAuthor
case doc.ReviewerID:
return docgov.RoleReviewer
return vocabulary.RoleReviewer
case doc.ApproverID:
return docgov.RoleApprover
return vocabulary.RoleApprover
}
return docgov.UserRole("")
return vocabulary.UserRole("")
}
@@ -12,6 +12,7 @@ import (
"chat-agent-system/internal/app/docflow"
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docrevision"
"chat-agent-system/internal/freedoc"
@@ -213,7 +214,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
// This UI has no edit-session heartbeat: release the request-scoped human lease on every exit
// (success, 409, projection failure) so a 15m residual cannot block a later squad run.
if kind == "controlled" && s.gov != nil {
if err := s.gov.AssertHumanCanEdit(r.Context(), docID, user.ID, docgov.RoleAuthor); err != nil {
if err := s.gov.AssertHumanCanEdit(r.Context(), docID, user.ID, vocabulary.RoleAuthor); err != nil {
writeDocGovError(w, err)
return
}
@@ -222,7 +223,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
holderID = "human_anon"
}
defer func() {
_, _ = s.gov.ReleaseLease(context.Background(), docID, string(docgov.LeaseHolderHuman), holderID)
_, _ = s.gov.ReleaseLease(context.Background(), docID, string(vocabulary.LeaseHolderHuman), holderID)
}()
}
@@ -250,7 +251,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
return
}
res, err := cmds.HumanEdit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: docgov.RoleAuthor}, docflow.HumanEditCommand{
res, err := cmds.HumanEdit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: vocabulary.RoleAuthor}, docflow.HumanEditCommand{
DocID: docID,
Request: docrevision.HumanRevisionRequest{
DocID: docID, Markdown: body.Markdown, ExpectedHeadSnapshot: expectedHead, ExpectedRevisionNo: expectedRev,
@@ -279,7 +280,7 @@ func (s *Server) handleDocRevisionPatchContent(w http.ResponseWriter, r *http.Re
if _, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
DocID: docID,
ActorID: user.ID,
ActorRole: docgov.RoleAuthor,
ActorRole: vocabulary.RoleAuthor,
ActorName: user.Name,
MarkdownBody: body.Markdown,
}); err != nil {
@@ -372,7 +373,7 @@ func (s *Server) handleDocRevisionCandidateDecision(w http.ResponseWriter, r *ht
writeJSONResponse(w, http.StatusServiceUnavailable, map[string]any{"error": "docrevision unavailable"})
return
}
st, err := cmds.AgentCommit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: docgov.RoleAuthor}, docflow.AgentCommitCommand{
st, err := cmds.AgentCommit(r.Context(), docflow.Principal{UserID: user.ID, Name: user.Name, Role: vocabulary.RoleAuthor}, docflow.AgentCommitCommand{
DocID: docID, CandidateID: cid,
Decision: docrevision.CommitAgentDecision{
Decision: body.Decision, Confirmed: body.Confirmed,
+21 -20
View File
@@ -16,6 +16,7 @@ import (
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/chat"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/httpx"
@@ -431,9 +432,9 @@ func (s *Server) handleRunTask(w http.ResponseWriter, r *http.Request) {
func writeTaskError(w http.ResponseWriter, err error, payload any) {
status := http.StatusInternalServerError
if errors.Is(err, docgov.ErrDocumentBusy) {
if errors.Is(err, vocabulary.ErrDocumentBusy) {
status = http.StatusConflict
} else if errors.Is(err, docgov.ErrDocumentLocked) || errors.Is(err, docgov.ErrForceEditNotAllowed) {
} else if errors.Is(err, vocabulary.ErrDocumentLocked) || errors.Is(err, vocabulary.ErrForceEditNotAllowed) {
status = http.StatusConflict
} else if errors.Is(err, projectworkflow.ErrPrerequisiteNotMet) {
status = http.StatusBadRequest
@@ -454,19 +455,19 @@ func writeDocGovError(w http.ResponseWriter, err error) {
})
return
}
if errors.Is(err, docgov.ErrDocumentBusy) {
if errors.Is(err, vocabulary.ErrDocumentBusy) {
writeSafeError(w, nil, http.StatusConflict, err.Error())
return
}
if errors.Is(err, docgov.ErrDocumentLocked) {
if errors.Is(err, vocabulary.ErrDocumentLocked) {
writeSafeError(w, nil, http.StatusConflict, err.Error())
return
}
if errors.Is(err, docgov.ErrDocumentNotFound) {
if errors.Is(err, vocabulary.ErrDocumentNotFound) {
writeSafeError(w, nil, http.StatusNotFound, err.Error())
return
}
if errors.Is(err, docgov.ErrLeaseNotHeld) || errors.Is(err, docgov.ErrLeaseExpired) {
if errors.Is(err, vocabulary.ErrLeaseNotHeld) || errors.Is(err, vocabulary.ErrLeaseExpired) {
writeSafeError(w, nil, http.StatusConflict, err.Error())
return
}
@@ -713,7 +714,7 @@ func (s *Server) handleDocGovSubmitReview(w http.ResponseWriter, r *http.Request
return
}
doc, err := s.gov.SubmitForReview(r.Context(), req.DocID, user.ID, docgov.RoleAuthor, user.Name)
doc, err := s.gov.SubmitForReview(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor, user.Name)
if err != nil {
writeSafeError(w, r, http.StatusBadRequest, "submit review failed: "+err.Error())
return
@@ -759,13 +760,13 @@ func (s *Server) handleDocGovReviewDecision(w http.ResponseWriter, r *http.Reque
var err error
if action == "approve" {
doc, err = s.gov.ReviewApprove(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, req.Comment)
doc, err = s.gov.ReviewApprove(r.Context(), req.DocID, user.ID, vocabulary.RoleReviewer, user.Name, req.Comment)
} else if action == "reject" {
reason := req.Reason
if reason == "" {
reason = req.Comment
}
doc, err = s.gov.ReviewReject(r.Context(), req.DocID, user.ID, docgov.RoleReviewer, user.Name, reason)
doc, err = s.gov.ReviewReject(r.Context(), req.DocID, user.ID, vocabulary.RoleReviewer, user.Name, reason)
} else {
writeSafeError(w, r, http.StatusBadRequest, "invalid action: must be approve or reject")
return
@@ -806,7 +807,7 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
return
}
p := docflow.Principal{UserID: user.ID, Name: user.Name, Role: docgov.RoleApprover}
p := docflow.Principal{UserID: user.ID, Name: user.Name, Role: vocabulary.RoleApprover}
var doc *docgov.DocumentRecord
var err error
@@ -818,7 +819,7 @@ func (s *Server) handleDocGovApproverDecision(w http.ResponseWriter, r *http.Req
reason = req.Comment
}
doc, err = cmds.Reject(r.Context(), p, docflow.DecisionCommand{
DocID: req.DocID, RejectReason: reason, RollbackTo: docgov.DocumentState(req.RollbackTo),
DocID: req.DocID, RejectReason: reason, RollbackTo: vocabulary.DocumentState(req.RollbackTo),
})
} else {
writeSafeError(w, r, http.StatusBadRequest, "invalid action: must be approve or reject")
@@ -890,7 +891,7 @@ func (s *Server) handleDocGovProposeModification(w http.ResponseWriter, r *http.
req.RiskAssessment,
)
if err != nil {
if errors.Is(err, docgov.ErrDocumentNotFound) {
if errors.Is(err, vocabulary.ErrDocumentNotFound) {
writeSafeError(w, r, http.StatusNotFound, "document not found")
return
}
@@ -933,11 +934,11 @@ func (s *Server) handleDocGovDecideProposal(w http.ResponseWriter, r *http.Reque
proposal, err := s.gov.DecideModificationProposal(r.Context(), req.ProposalID, user.ID, req.Approve, req.Reason)
if err != nil {
if errors.Is(err, docgov.ErrProposalNotFound) {
if errors.Is(err, vocabulary.ErrProposalNotFound) {
writeSafeError(w, r, http.StatusNotFound, "proposal not found")
return
}
if errors.Is(err, docgov.ErrUnauthorizedActor) {
if errors.Is(err, vocabulary.ErrUnauthorizedActor) {
writeSafeError(w, r, http.StatusForbidden, "decide proposal failed: "+err.Error())
return
}
@@ -1004,9 +1005,9 @@ func (s *Server) handleDocGovAddComment(w http.ResponseWriter, r *http.Request)
return
}
cType := docgov.CommentType(req.CommentType)
cType := vocabulary.CommentType(req.CommentType)
if cType == "" {
cType = docgov.CommentGeneral
cType = vocabulary.CommentGeneral
}
comment, err := s.gov.AddComment(
@@ -1302,7 +1303,7 @@ func (s *Server) handleDocGovSaveContent(w http.ResponseWriter, r *http.Request)
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
return
}
role := docgov.RoleAuthor
role := vocabulary.RoleAuthor
res, err := s.gov.SaveDocumentContent(r.Context(), docgov.ContentSaveRequest{
DocID: req.DocID,
ActorID: user.ID,
@@ -1339,8 +1340,8 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request
writeSafeError(w, r, http.StatusServiceUnavailable, "docgov engine not initialized")
return
}
if docgov.LeaseHolderType(req.HolderType) == docgov.LeaseHolderHuman {
if err := s.gov.CheckHumanCanEdit(r.Context(), req.DocID, user.ID, docgov.RoleAuthor); err != nil {
if vocabulary.LeaseHolderType(req.HolderType) == vocabulary.LeaseHolderHuman {
if err := s.gov.CheckHumanCanEdit(r.Context(), req.DocID, user.ID, vocabulary.RoleAuthor); err != nil {
writeDocGovError(w, err)
return
}
@@ -1353,7 +1354,7 @@ func (s *Server) handleDocGovAcquireLease(w http.ResponseWriter, r *http.Request
ttl := time.Duration(req.TTLSeconds) * time.Second
doc, err := s.gov.AcquireLease(r.Context(), docgov.LeaseAcquireRequest{
DocID: req.DocID,
HolderType: docgov.LeaseHolderType(req.HolderType),
HolderType: vocabulary.LeaseHolderType(req.HolderType),
HolderID: req.HolderID,
WorkDir: workDir,
TTL: ttl,
+2 -2
View File
@@ -8,7 +8,7 @@ import (
"strconv"
"strings"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/projectworkflow"
)
@@ -52,7 +52,7 @@ func (s *Server) lineageDocVisible(ctx context.Context, projectID, userID int64,
func (s *Server) lineageDocPlacement(ctx context.Context, projectID int64, docID string) (taskKey string, placed bool, ok bool) {
if s.gov != nil {
doc, err := s.gov.GetDocument(ctx, docID)
if err != nil && !errors.Is(err, docgov.ErrDocumentNotFound) {
if err != nil && !errors.Is(err, vocabulary.ErrDocumentNotFound) {
return "", false, false
}
if err == nil && doc != nil {
+6 -5
View File
@@ -10,6 +10,7 @@ import (
"time"
"chat-agent-system/internal/app/docflow"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/docquery"
"chat-agent-system/internal/docrevision"
@@ -132,7 +133,7 @@ func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.R
}
if errors.Is(err, docquery.ErrClosed) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{
"error_code": docgov.ErrCodeDocumentNotFound, "message": "document not found",
"error_code": vocabulary.ErrCodeDocumentNotFound, "message": "document not found",
})
return
}
@@ -150,7 +151,7 @@ func (s *Server) handleDocGovGetDocumentContent(w http.ResponseWriter, r *http.R
func (s *Server) loadControlledDocumentContent(ctx context.Context, docID string) (*docgov.DocumentContent, error) {
if s == nil || s.gov == nil {
return nil, docgov.ErrDocumentNotFound
return nil, vocabulary.ErrDocumentNotFound
}
svc := &docquery.Service{Gov: s.gov, Rev: s.docRev(), RepoRoot: s.repoRoot()}
view, err := svc.Get(ctx, docquery.Principal{UserID: 1}, docquery.DocRef{DocID: docID})
@@ -191,7 +192,7 @@ func writeControlledContentError(w http.ResponseWriter, r *http.Request, err err
var ce *docgov.ContentError
if errors.As(err, &ce) {
status := http.StatusConflict
if ce.Code == docgov.ErrCodeDocumentNotFound {
if ce.Code == vocabulary.ErrCodeDocumentNotFound {
status = http.StatusNotFound
}
writeJSONResponse(w, status, map[string]any{
@@ -207,9 +208,9 @@ func writeControlledContentError(w http.ResponseWriter, r *http.Request, err err
})
return
}
if errors.Is(err, docgov.ErrDocumentNotFound) {
if errors.Is(err, vocabulary.ErrDocumentNotFound) {
writeJSONResponse(w, http.StatusNotFound, map[string]any{
"error_code": docgov.ErrCodeDocumentNotFound, "message": err.Error(),
"error_code": vocabulary.ErrCodeDocumentNotFound, "message": err.Error(),
"retryable": false, "needs_admin": false, "correlation_id": corr,
})
return
+3 -2
View File
@@ -13,6 +13,7 @@ import (
"chat-agent-system/internal/agent"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/contracts/vocabulary"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/projectinstance"
@@ -409,7 +410,7 @@ func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRu
return nil, nil
}
doc, err := s.gov.GetDocument(ctx, docID)
if errors.Is(err, docgov.ErrDocumentNotFound) {
if errors.Is(err, vocabulary.ErrDocumentNotFound) {
return nil, nil
}
if err != nil {
@@ -419,7 +420,7 @@ func (s *Server) preflightTaskDocumentBusy(ctx context.Context, req createTaskRu
return nil, nil
}
// 已结束的小队 run 由 AcquireLease 的孤儿回收逻辑处理,不在这里误拦截。
if doc.LeaseHolderType == docgov.LeaseHolderSquad && s.taskRuns != nil {
if doc.LeaseHolderType == vocabulary.LeaseHolderSquad && s.taskRuns != nil {
if run, runErr := s.taskRuns.GetRun(doc.LeaseHolderID); runErr == nil && run.Status != "running" {
return nil, nil
}