feat(docgov): lock document duties and project duty_policy

Add LockDocumentDuty for PG/memory and expose duty_policy on workflow
document summaries for later UI marking.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-17 09:59:38 +00:00
co-authored by Cursor
parent 7d3474816b
commit 89484177ee
3 changed files with 49 additions and 3 deletions
+40 -1
View File
@@ -218,6 +218,43 @@ func (e *GovernanceEngine) CreateOrGetDocument(
return doc, nil return doc, nil
} }
// LockDocumentDuty 将文档职责钉住为实例治理结论;heal/revoke 跳过 duty_locked 行。
// 不改写 author/reviewer/approver 列——那些在 CreateOrGetDocument 时已由调用方写入。
func (e *GovernanceEngine) LockDocumentDuty(ctx context.Context, docID string, dutyPolicy string) error {
docID = strings.TrimSpace(docID)
if docID == "" {
return fmt.Errorf("docgov: empty doc_id")
}
dutyPolicy = strings.TrimSpace(dutyPolicy)
e.mu.Lock()
defer e.mu.Unlock()
doc, err := e.ensureDocumentLocked(ctx, docID)
if err != nil {
return err
}
now := time.Now()
doc.DutyLocked = true
doc.DutyPolicy = dutyPolicy
doc.UpdatedAt = now
if e.pool != nil {
tag, execErr := e.pool.Exec(ctx, `
UPDATE document_records
SET duty_locked = true, duty_policy = $2, updated_at = $3
WHERE doc_id = $1
`, docID, dutyPolicy, now)
if execErr != nil {
return fmt.Errorf("docgov: lock document duty: %w", execErr)
}
if tag.RowsAffected() == 0 {
return ErrDocumentNotFound
}
}
return nil
}
// MarkDocumentContentUpdated 当正文生成或更新时标记正文哈希及是否脱离模板状态。 // MarkDocumentContentUpdated 当正文生成或更新时标记正文哈希及是否脱离模板状态。
// 未预加载时从 DB 补偿装入;已是目标哈希则幂等成功,保证 repair 重试也能同步记录。 // 未预加载时从 DB 补偿装入;已是目标哈希则幂等成功,保证 repair 重试也能同步记录。
func (e *GovernanceEngine) MarkDocumentContentUpdated(ctx context.Context, docID string, contentSHA256 string, isTemplateOnly bool) error { func (e *GovernanceEngine) MarkDocumentContentUpdated(ctx context.Context, docID string, contentSHA256 string, isTemplateOnly bool) error {
@@ -1043,7 +1080,8 @@ func (e *GovernanceEngine) loadDocumentFromDBLocked(ctx context.Context, docID s
last_rejected_reason, last_rejected_by_role, last_rejected_by_name, last_rejected_at, last_rejected_reason, last_rejected_by_role, last_rejected_by_name, last_rejected_at,
approved_at, config, created_at, updated_at, approved_at, config, created_at, updated_at,
COALESCE(lease_holder_type, ''), COALESCE(lease_holder_id, ''), COALESCE(lease_work_dir, ''), COALESCE(lease_holder_type, ''), COALESCE(lease_holder_id, ''), COALESCE(lease_work_dir, ''),
lease_expires_at, lease_heartbeat_at lease_expires_at, lease_heartbeat_at,
COALESCE(duty_locked, false), COALESCE(duty_policy, '')
FROM document_records FROM document_records
WHERE doc_id = $1 WHERE doc_id = $1
`, docID) `, docID)
@@ -1062,6 +1100,7 @@ func (e *GovernanceEngine) loadDocumentFromDBLocked(ctx context.Context, docID s
&doc.LastRejectedReason, &doc.LastRejectedByRole, &doc.LastRejectedByName, &doc.LastRejectedAt, &doc.LastRejectedReason, &doc.LastRejectedByRole, &doc.LastRejectedByName, &doc.LastRejectedAt,
&doc.ApprovedAt, &cfgJSON, &doc.CreatedAt, &doc.UpdatedAt, &doc.ApprovedAt, &cfgJSON, &doc.CreatedAt, &doc.UpdatedAt,
&leaseType, &leaseID, &leaseWD, &doc.LeaseExpiresAt, &doc.LeaseHeartbeatAt, &leaseType, &leaseID, &leaseWD, &doc.LeaseExpiresAt, &doc.LeaseHeartbeatAt,
&doc.DutyLocked, &doc.DutyPolicy,
) )
if err != nil { if err != nil {
return nil, err return nil, err
+5 -2
View File
@@ -36,6 +36,7 @@ type WorkflowDocumentSummary struct {
ReviewerName string `json:"reviewer_name"` ReviewerName string `json:"reviewer_name"`
ApproverID int64 `json:"approver_id"` ApproverID int64 `json:"approver_id"`
ApproverName string `json:"approver_name"` ApproverName string `json:"approver_name"`
DutyPolicy string `json:"duty_policy,omitempty"`
} }
// DocumentContent 受控文档正文读取结果。 // DocumentContent 受控文档正文读取结果。
@@ -67,7 +68,8 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p
lease_expires_at, lease_heartbeat_at, lease_expires_at, lease_heartbeat_at,
COALESCE(author_id, 0), COALESCE(author_name, ''), COALESCE(author_id, 0), COALESCE(author_name, ''),
COALESCE(reviewer_id, 0), COALESCE(reviewer_name, ''), COALESCE(reviewer_id, 0), COALESCE(reviewer_name, ''),
COALESCE(approver_id, 0), COALESCE(approver_name, '') COALESCE(approver_id, 0), COALESCE(approver_name, ''),
COALESCE(duty_policy, '')
FROM document_records FROM document_records
WHERE project_id = $1 AND task_key <> '' WHERE project_id = $1 AND task_key <> ''
ORDER BY task_key ASC, doc_id ASC ORDER BY task_key ASC, doc_id ASC
@@ -86,7 +88,7 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p
&item.IsTemplateOnly, &item.ContentSHA256, &item.WorkDir, &item.Version, &updatedAt, &item.IsTemplateOnly, &item.ContentSHA256, &item.WorkDir, &item.Version, &updatedAt,
&item.LeaseHolderType, &item.LeaseHolderID, &item.LeaseExpiresAt, &item.LeaseHeartbeatAt, &item.LeaseHolderType, &item.LeaseHolderID, &item.LeaseExpiresAt, &item.LeaseHeartbeatAt,
&item.AuthorID, &item.AuthorName, &item.ReviewerID, &item.ReviewerName, &item.AuthorID, &item.AuthorName, &item.ReviewerID, &item.ReviewerName,
&item.ApproverID, &item.ApproverName, &item.ApproverID, &item.ApproverName, &item.DutyPolicy,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -124,6 +126,7 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p
ReviewerName: doc.ReviewerName, ReviewerName: doc.ReviewerName,
ApproverID: doc.ApproverID, ApproverID: doc.ApproverID,
ApproverName: doc.ApproverName, ApproverName: doc.ApproverName,
DutyPolicy: doc.DutyPolicy,
}) })
} }
} }
+4
View File
@@ -166,6 +166,10 @@ type DocumentRecord struct {
ApproverID int64 `json:"approver_id"` ApproverID int64 `json:"approver_id"`
ApproverName string `json:"approver_name"` ApproverName string `json:"approver_name"`
// DutyLocked 为 true 时,职责权威来自工作流实例规划,heal/revoke 不得覆盖。
DutyLocked bool `json:"duty_locked"`
DutyPolicy string `json:"duty_policy,omitempty"`
// 认领状态 (Claiming State) // 认领状态 (Claiming State)
AuthorClaimed bool `json:"author_claimed"` // 编制人是否已认领 (非模板状态查看/编辑后为 true) AuthorClaimed bool `json:"author_claimed"` // 编制人是否已认领 (非模板状态查看/编辑后为 true)
AuthorClaimedAt *time.Time `json:"author_claimed_at"` AuthorClaimedAt *time.Time `json:"author_claimed_at"`