From 381b56dafc4a841d124f594189983c75851faffc Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 22 Sep 2026 21:44:57 +0800 Subject: [PATCH 1/2] refactor(docgov): converge shared lifecycle vocab into contracts Introduce internal/contracts/vocabulary for state/role/error sentinels, alias through docgov, and retarget hot production consumers so coordinator no longer imports docgov for vocabulary-only use. --- ...026-09-22-docgov-vocabulary-convergence.md | 93 +++++++++++++ ...2026-09-20-module-development-standards.md | 2 +- .doc/system-design/module-boundaries.yaml | 20 ++- internal/app/docadvance/consumer.go | 7 +- internal/app/docflow/commands.go | 5 +- internal/app/duty_reassign_adapters.go | 5 +- internal/app/trace_bridge.go | 5 +- internal/bizdev/service.go | 25 ++-- internal/chat/action_executor.go | 11 +- .../contracts/vocabulary/doc_lifecycle.go | 87 ++++++++++++ internal/coordinator/engine.go | 12 +- internal/docgov/types.go | 129 ++++++++---------- internal/server/auth_helpers.go | 16 +-- internal/server/document_revision_handlers.go | 11 +- internal/server/handlers.go | 41 +++--- internal/server/lineage_access.go | 4 +- internal/server/project_doc_handlers.go | 11 +- internal/server/task_run_handlers.go | 5 +- 18 files changed, 337 insertions(+), 152 deletions(-) create mode 100644 .doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md create mode 100644 internal/contracts/vocabulary/doc_lifecycle.go diff --git a/.doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md b/.doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md new file mode 100644 index 00000000..eb8bd788 --- /dev/null +++ b/.doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md @@ -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: "" +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。 + diff --git a/.doc/system-design/2026-09-20-module-development-standards.md b/.doc/system-design/2026-09-20-module-development-standards.md index 2f3e715b..203eec45 100644 --- a/.doc/system-design/2026-09-20-module-development-standards.md +++ b/.doc/system-design/2026-09-20-module-development-standards.md @@ -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. 故障隔离红线 diff --git a/.doc/system-design/module-boundaries.yaml b/.doc/system-design/module-boundaries.yaml index ab8e9d2b..d9f22910 100644 --- a/.doc/system-design/module-boundaries.yaml +++ b/.doc/system-design/module-boundaries.yaml @@ -14,6 +14,7 @@ imports: bizdev: - authz - config + - contracts/vocabulary - ctxmgr - db - docduty @@ -21,10 +22,10 @@ imports: - docrevision - freedoc - kb + - materialindex - maturity - projectcontext - projectworkflow - - materialindex - quote - squad - storage @@ -34,6 +35,7 @@ imports: - app/chatflow - app/intentflow - app/stageflow + - contracts/vocabulary - coordinator - ctxmgr - docgov @@ -44,11 +46,12 @@ imports: - stream - taskrun config: [] + contracts/vocabulary: [] coordinator: - agent - app/stageflow + - contracts/vocabulary - ctxmgr - - docgov - intent - kb - maturity @@ -78,6 +81,7 @@ imports: - authz - bizdev - 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 diff --git a/internal/app/docadvance/consumer.go b/internal/app/docadvance/consumer.go index b9a9f151..c76781cc 100644 --- a/internal/app/docadvance/consumer.go +++ b/internal/app/docadvance/consumer.go @@ -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 == "" { diff --git a/internal/app/docflow/commands.go b/internal/app/docflow/commands.go index d9e3c427..945374b1 100644 --- a/internal/app/docflow/commands.go +++ b/internal/app/docflow/commands.go @@ -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 { diff --git a/internal/app/duty_reassign_adapters.go b/internal/app/duty_reassign_adapters.go index 2d6ca872..ac39a79f 100644 --- a/internal/app/duty_reassign_adapters.go +++ b/internal/app/duty_reassign_adapters.go @@ -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 } diff --git a/internal/app/trace_bridge.go b/internal/app/trace_bridge.go index 409308f7..1a2aa2f8 100644 --- a/internal/app/trace_bridge.go +++ b/internal/app/trace_bridge.go @@ -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 diff --git a/internal/bizdev/service.go b/internal/bizdev/service.go index ba88de00..349ff10d 100644 --- a/internal/bizdev/service.go +++ b/internal/bizdev/service.go @@ -14,8 +14,11 @@ import ( "strings" "time" + "github.com/jackc/pgx/v5/pgxpool" + "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" @@ -30,8 +33,6 @@ import ( "chat-agent-system/internal/storage" "chat-agent-system/internal/stream" "chat-agent-system/internal/workflowdef" - - "github.com/jackc/pgx/v5/pgxpool" ) type Service struct { @@ -528,7 +529,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") @@ -581,7 +582,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 { @@ -707,7 +708,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 { @@ -720,14 +721,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 @@ -797,11 +798,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, @@ -847,7 +848,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() } } @@ -855,7 +856,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{ diff --git a/internal/chat/action_executor.go b/internal/chat/action_executor.go index 571d005d..ecf54eec 100644 --- a/internal/chat/action_executor.go +++ b/internal/chat/action_executor.go @@ -9,6 +9,7 @@ import ( "chat-agent-system/internal/app/intentflow" "chat-agent-system/internal/app/stageflow" + "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/coordinator" "chat-agent-system/internal/docgov" "chat-agent-system/internal/taskrun" @@ -59,7 +60,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 } @@ -69,7 +70,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 } @@ -83,13 +84,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 } diff --git a/internal/contracts/vocabulary/doc_lifecycle.go b/internal/contracts/vocabulary/doc_lifecycle.go new file mode 100644 index 00000000..e3e08048 --- /dev/null +++ b/internal/contracts/vocabulary/doc_lifecycle.go @@ -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" +) diff --git a/internal/coordinator/engine.go b/internal/coordinator/engine.go index 5f5c580e..0c6661f4 100644 --- a/internal/coordinator/engine.go +++ b/internal/coordinator/engine.go @@ -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{ diff --git a/internal/docgov/types.go b/internal/docgov/types.go index 5a1dd710..acbb4c08 100644 --- a/internal/docgov/types.go +++ b/internal/docgov/types.go @@ -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"` diff --git a/internal/server/auth_helpers.go b/internal/server/auth_helpers.go index 407f2133..595face3 100644 --- a/internal/server/auth_helpers.go +++ b/internal/server/auth_helpers.go @@ -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("") } diff --git a/internal/server/document_revision_handlers.go b/internal/server/document_revision_handlers.go index 9001b19d..155731a0 100644 --- a/internal/server/document_revision_handlers.go +++ b/internal/server/document_revision_handlers.go @@ -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, diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 1b99a19c..cd5501f6 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -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, diff --git a/internal/server/lineage_access.go b/internal/server/lineage_access.go index 0b197d42..ef24fa7b 100644 --- a/internal/server/lineage_access.go +++ b/internal/server/lineage_access.go @@ -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 { diff --git a/internal/server/project_doc_handlers.go b/internal/server/project_doc_handlers.go index 056ca907..e49a4e61 100644 --- a/internal/server/project_doc_handlers.go +++ b/internal/server/project_doc_handlers.go @@ -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 diff --git a/internal/server/task_run_handlers.go b/internal/server/task_run_handlers.go index 435616c2..ee9b1f66 100644 --- a/internal/server/task_run_handlers.go +++ b/internal/server/task_run_handlers.go @@ -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 } From 668ed249ef7fd6983e0f1d9d2c971d8ffd305c9b Mon Sep 17 00:00:00 2001 From: WeiCHEN Date: Tue, 22 Sep 2026 21:44:57 +0800 Subject: [PATCH 2/2] docs(ops): pin docgov-vocabulary head_sha to tip --- .doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md b/.doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md index eb8bd788..1a8862ff 100644 --- a/.doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md +++ b/.doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md @@ -24,7 +24,7 @@ write_scope: - ".doc/system-design/module-boundaries.yaml" - ".doc/ops/short-term/2026-09-22-docgov-vocabulary-convergence.md" allowed_environments: [] -head_sha: "" +head_sha: "381b56dafc4a841d124f594189983c75851faffc" mutex: docgov review_state: pending integration_state: pending