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:
@@ -218,6 +218,43 @@ func (e *GovernanceEngine) CreateOrGetDocument(
|
||||
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 当正文生成或更新时标记正文哈希及是否脱离模板状态。
|
||||
// 未预加载时从 DB 补偿装入;已是目标哈希则幂等成功,保证 repair 重试也能同步记录。
|
||||
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,
|
||||
approved_at, config, created_at, updated_at,
|
||||
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
|
||||
WHERE doc_id = $1
|
||||
`, docID)
|
||||
@@ -1062,6 +1100,7 @@ func (e *GovernanceEngine) loadDocumentFromDBLocked(ctx context.Context, docID s
|
||||
&doc.LastRejectedReason, &doc.LastRejectedByRole, &doc.LastRejectedByName, &doc.LastRejectedAt,
|
||||
&doc.ApprovedAt, &cfgJSON, &doc.CreatedAt, &doc.UpdatedAt,
|
||||
&leaseType, &leaseID, &leaseWD, &doc.LeaseExpiresAt, &doc.LeaseHeartbeatAt,
|
||||
&doc.DutyLocked, &doc.DutyPolicy,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -36,6 +36,7 @@ type WorkflowDocumentSummary struct {
|
||||
ReviewerName string `json:"reviewer_name"`
|
||||
ApproverID int64 `json:"approver_id"`
|
||||
ApproverName string `json:"approver_name"`
|
||||
DutyPolicy string `json:"duty_policy,omitempty"`
|
||||
}
|
||||
|
||||
// DocumentContent 受控文档正文读取结果。
|
||||
@@ -67,7 +68,8 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p
|
||||
lease_expires_at, lease_heartbeat_at,
|
||||
COALESCE(author_id, 0), COALESCE(author_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
|
||||
WHERE project_id = $1 AND task_key <> ''
|
||||
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.LeaseHolderType, &item.LeaseHolderID, &item.LeaseExpiresAt, &item.LeaseHeartbeatAt,
|
||||
&item.AuthorID, &item.AuthorName, &item.ReviewerID, &item.ReviewerName,
|
||||
&item.ApproverID, &item.ApproverName,
|
||||
&item.ApproverID, &item.ApproverName, &item.DutyPolicy,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -124,6 +126,7 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p
|
||||
ReviewerName: doc.ReviewerName,
|
||||
ApproverID: doc.ApproverID,
|
||||
ApproverName: doc.ApproverName,
|
||||
DutyPolicy: doc.DutyPolicy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,10 @@ type DocumentRecord struct {
|
||||
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"`
|
||||
|
||||
Reference in New Issue
Block a user