package docgov import ( "time" "chat-agent-system/internal/contracts/vocabulary" ) // 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 ( 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. type ContentError struct { Code string Message string Retryable bool NeedsAdmin bool RecordSHA string FileSHA string HeadSHA string Class string cause error } func (e *ContentError) Error() string { if e == nil { return "" } if e.Message != "" { return e.Message } return e.Code } func (e *ContentError) Unwrap() error { if e == nil { return nil } return e.cause } func (e *ContentError) ErrorCode() string { if e == nil || e.Code == "" { return "DOCUMENT_CONTENT_ERROR" } return e.Code } func NewContentError(code string, cause error, retryable, needsAdmin bool) *ContentError { msg := code if cause != nil { msg = cause.Error() } return &ContentError{Code: code, Message: msg, Retryable: retryable, NeedsAdmin: needsAdmin, cause: cause} } // DocumentBusyError 携带租约占用详情,便于 HTTP 409 与 UI 提示 type DocumentBusyError struct { DocID string `json:"doc_id"` HolderType LeaseHolderType `json:"holder_type"` HolderID string `json:"holder_id"` HolderDisplayName string `json:"holder_display_name,omitempty"` WorkDir string `json:"work_dir,omitempty"` ExpiresAt time.Time `json:"expires_at"` Reason string `json:"reason"` } func (e *DocumentBusyError) Error() string { if e.Reason != "" { return e.Reason } return ErrDocumentBusy.Error() } func (e *DocumentBusyError) Unwrap() error { return ErrDocumentBusy } func (e *DocumentBusyError) ErrorCode() string { return "DOC_BUSY" } // DocumentConfig 受控文档流转与退回配置 type DocumentConfig struct { RejectRollbackTo DocumentState `json:"reject_rollback_to"` // 驳回退回目标: drafting 或 reviewing AllowApproverToReviewer bool `json:"allow_approver_to_reviewer"` // 是否允许批准人退回至审核阶段 RequireAuthorConfirmOnReject bool `json:"require_author_confirm_on_reject"` // 驳回退回编制时是否需编制人确认已获悉 } func DefaultDocumentConfig() DocumentConfig { return DocumentConfig{ RejectRollbackTo: StateDrafting, AllowApproverToReviewer: true, RequireAuthorConfirmOnReject: true, } } // DocumentRecord 受控文档治理台账实体 type DocumentRecord struct { ID int64 `json:"id"` DocID string `json:"doc_id"` // 业务唯一ID,如 doc_proj1_feasibility_reg_v1 ProjectID int64 `json:"project_id"` // 项目ID TaskKey string `json:"task_key"` // 任务标识: feasibility_reg, intake, cdo_quote Title string `json:"title"` // 标题 Stage string `json:"stage"` // 阶段: V0.5_feasibility Version int `json:"version"` // 版本号 CurrentState DocumentState `json:"current_state"` // drafting, reviewing, approving, approved IsLocked bool `json:"is_locked"` // reviewing/approving/approved 期间锁定 IsTemplateOnly bool `json:"is_template_only"` // 是否仅为骨架模板 (模板状态即使查看也不算认领) ContentSHA256 string `json:"content_sha256"` // 当前版本正文哈希 WorkDir string `json:"work_dir"` // 关联工作区相对路径 // 三级法定责任人 (各有且仅有一个) AuthorID int64 `json:"author_id"` AuthorName string `json:"author_name"` ReviewerID int64 `json:"reviewer_id"` ReviewerName string `json:"reviewer_name"` ApproverID int64 `json:"approver_id"` ApproverName string `json:"approver_name"` // DutyLocked 为 true 时,职责权威来自工作流实例规划,heal/revoke 不得覆盖。 DutyLocked bool `json:"duty_locked"` DutyPolicy string `json:"duty_policy,omitempty"` // 认领状态 (Claiming State) AuthorClaimed bool `json:"author_claimed"` // 编制人是否已认领 (非模板状态查看/编辑后为 true) AuthorClaimedAt *time.Time `json:"author_claimed_at"` AuthorLastViewedAt *time.Time `json:"author_last_viewed_at"` AuthorLastEditedAt *time.Time `json:"author_last_edited_at"` ReviewerClaimed bool `json:"reviewer_claimed"` // 审核人是否已认领审核 ReviewerClaimedAt *time.Time `json:"reviewer_claimed_at"` ApproverClaimed bool `json:"approver_claimed"` // 批准人是否已认领批准 ApproverClaimedAt *time.Time `json:"approver_claimed_at"` // 驳回与决议记录 LastRejectedReason string `json:"last_rejected_reason"` LastRejectedByRole string `json:"last_rejected_by_role"` LastRejectedByName string `json:"last_rejected_by_name"` LastRejectedAt *time.Time `json:"last_rejected_at"` ApprovedAt *time.Time `json:"approved_at"` ReviewSnapshotID int64 `json:"review_snapshot_id,omitempty"` // 编辑租约 (运行时互斥,正交于 IsLocked) LeaseHolderType LeaseHolderType `json:"lease_holder_type,omitempty"` LeaseHolderID string `json:"lease_holder_id,omitempty"` LeaseWorkDir string `json:"lease_work_dir,omitempty"` LeaseExpiresAt *time.Time `json:"lease_expires_at,omitempty"` LeaseHeartbeatAt *time.Time `json:"lease_heartbeat_at,omitempty"` LeaseFencingToken string `json:"lease_fencing_token,omitempty"` Config DocumentConfig `json:"config"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // HasActiveLease 判断文档是否持有未过期的编辑租约 func (d *DocumentRecord) HasActiveLease(now time.Time) bool { if d == nil || d.LeaseHolderType == LeaseHolderNone || d.LeaseHolderType == "" { return false } if d.LeaseExpiresAt == nil { return false } return d.LeaseExpiresAt.After(now) } // ClearLease 清空租约字段(内存) func (d *DocumentRecord) ClearLease() { d.LeaseHolderType = LeaseHolderNone d.LeaseHolderID = "" d.LeaseWorkDir = "" d.LeaseExpiresAt = nil d.LeaseHeartbeatAt = nil d.LeaseFencingToken = "" } // DocumentComment 批注与修改意见实体 type DocumentComment struct { ID int64 `json:"id"` DocID string `json:"doc_id"` UserID int64 `json:"user_id"` UserName string `json:"user_name"` UserRole UserRole `json:"user_role"` // author, reviewer, approver TargetSection string `json:"target_section"` // 目标章节锚点,如 "## 结论" CommentType CommentType `json:"comment_type"` // comment, suggestion, reject_reason Content string `json:"content"` // 意见内容 Status string `json:"status"` // open, resolved, accepted CreatedAt time.Time `json:"created_at"` } // ProposalStatus 变更提案状态 type ProposalStatus string const ( ProposalPendingAuthorApproval ProposalStatus = "pending_author_approval" // 待编制人拍板 ProposalApprovedByAuthor ProposalStatus = "approved_by_author" // 编制人已同意修改 ProposalRejectedByAuthor ProposalStatus = "rejected_by_author" // 编制人已拒绝修改 ProposalApplied ProposalStatus = "applied" // 已应用执行 ) // ChangeProposal 针对已认领文档的智能体修改变更提案实体 type ChangeProposal struct { ID int64 `json:"id"` ProposalID string `json:"proposal_id"` DocID string `json:"doc_id"` ProjectID int64 `json:"project_id"` TriggeredBy string `json:"triggered_by"` // agent_squad, stage_sync, upstream_change ProposedChanges string `json:"proposed_changes"` // 有哪些地方要改 (变更内容与对比摘要) ImpactAnalysis string `json:"impact_analysis"` // 影响会是什么 (范围、上下游依赖) RiskAssessment string `json:"risk_assessment"` // 可能产生的风险 (合规、工时、预算) Status ProposalStatus `json:"status"` // pending_author_approval... AuthorDecisionReason string `json:"author_decision_reason"` // 编制人批准/拒绝原因 DecidedAt *time.Time `json:"decided_at"` CreatedAt time.Time `json:"created_at"` } // AuditLog 文档治理生命周期审计跟踪 type AuditLog struct { ID int64 `json:"id"` DocID string `json:"doc_id"` Action string `json:"action"` // view, edit, claim, submit_review, review_approve, review_reject... ActorID int64 `json:"actor_id"` ActorName string `json:"actor_name"` ActorRole UserRole `json:"actor_role"` FromState DocumentState `json:"from_state"` ToState DocumentState `json:"to_state"` Details map[string]any `json:"details"` CreatedAt time.Time `json:"created_at"` }