Files
Agent-System/internal/docgov/repository.go
T
WeiCHEN 9c7a5be209 feat(arch): clear G1 U3/U4 sql_write and path exceptions
Move document_records duty-slot writes into docgov, project ledger
heads through docgov/freedoc ports, and workspace deliverable I/O
through DeliverableStore so arch-check baseline drops to sql_write:1
path:13.
2026-09-22 21:40:12 +08:00

137 lines
4.7 KiB
Go

package docgov
import (
"context"
"fmt"
"time"
"chat-agent-system/internal/app/uow"
"github.com/jackc/pgx/v5"
)
// Repository is the document_records writer bound to a UnitOfWork scope.
type Repository struct{}
func (Repository) InsertRoot(ctx context.Context, scope uow.TxScope, rec DocumentRecord) error {
tx, err := uow.AsPG(scope)
if err != nil {
return err
}
if rec.CreatedAt.IsZero() {
rec.CreatedAt = time.Now().UTC()
}
if rec.UpdatedAt.IsZero() {
rec.UpdatedAt = rec.CreatedAt
}
if rec.Stage == "" {
rec.Stage = "V0.5_feasibility"
}
if rec.CurrentState == "" {
rec.CurrentState = StateDrafting
}
if rec.Title == "" {
rec.Title = rec.DocID
}
if rec.TaskKey == "" {
rec.TaskKey = "intake"
}
_, err = tx.Exec(ctx, `
INSERT INTO document_records (
doc_id, project_id, task_key, title, stage, version,
current_state, is_locked, is_template_only, content_sha256, work_dir,
author_name, reviewer_name, approver_name, created_at, updated_at
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`,
rec.DocID, rec.ProjectID, rec.TaskKey, rec.Title, rec.Stage, rec.Version,
string(rec.CurrentState), rec.IsLocked, rec.IsTemplateOnly, rec.ContentSHA256, rec.WorkDir,
rec.AuthorName, rec.ReviewerName, rec.ApproverName, rec.CreatedAt, rec.UpdatedAt)
if err != nil {
return fmt.Errorf("docgov: insert root: %w", err)
}
return nil
}
func (Repository) GetForUpdate(ctx context.Context, scope uow.TxScope, docID string) (*DocumentRecord, error) {
tx, err := uow.AsPG(scope)
if err != nil {
return nil, err
}
var rec DocumentRecord
var state string
err = tx.QueryRow(ctx, `
SELECT doc_id, project_id, task_key, title, stage, version, current_state,
is_locked, is_template_only, content_sha256, work_dir,
COALESCE(author_id, 0), COALESCE(reviewer_id, 0), COALESCE(approver_id, 0)
FROM document_records WHERE doc_id=$1 FOR UPDATE`, docID).Scan(
&rec.DocID, &rec.ProjectID, &rec.TaskKey, &rec.Title, &rec.Stage, &rec.Version, &state,
&rec.IsLocked, &rec.IsTemplateOnly, &rec.ContentSHA256, &rec.WorkDir,
&rec.AuthorID, &rec.ReviewerID, &rec.ApproverID)
if err != nil {
return nil, fmt.Errorf("docgov: lock document: %w", err)
}
rec.CurrentState = DocumentState(state)
return &rec, nil
}
func (Repository) UpdateState(ctx context.Context, scope uow.TxScope, rec DocumentRecord) error {
tx, err := uow.AsPG(scope)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `
UPDATE document_records
SET current_state=$2, is_locked=$3, content_sha256=COALESCE(NULLIF($4,''), content_sha256),
updated_at=$5
WHERE doc_id=$1`,
rec.DocID, string(rec.CurrentState), rec.IsLocked, rec.ContentSHA256, time.Now().UTC())
if err != nil {
return fmt.Errorf("docgov: update state: %w", err)
}
return nil
}
func (Repository) ProjectHead(ctx context.Context, scope uow.TxScope, docID string, versionID, snapshotID int64, revisionNo int, sha string) error {
return (Repository{}).ProjectHeadFull(ctx, scope, docID, &versionID, &snapshotID, revisionNo, 0, sha)
}
// ProjectHeadFull projects ledger head pointers, optionally updating revision_schema_version when schemaVer > 0.
func (Repository) ProjectHeadFull(ctx context.Context, scope uow.TxScope, docID string, versionID, snapshotID *int64, revisionNo, schemaVer int, sha string) error {
tx, err := uow.AsPG(scope)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `
UPDATE document_records
SET ledger_version_id=$2, head_snapshot_id=$3, head_revision_no=$4,
revision_schema_version=CASE WHEN $5 > 0 THEN $5 ELSE revision_schema_version END,
row_revision=COALESCE(row_revision,0)+1,
content_sha256=COALESCE(NULLIF($6,''), content_sha256),
updated_at=now()
WHERE doc_id=$1`,
docID, versionID, snapshotID, revisionNo, schemaVer, sha)
if err != nil {
return fmt.Errorf("docgov: project head: %w", err)
}
return nil
}
// ProjectHeadNoReplace moves ledger pointers only forward. Equal or newer heads stay.
func (Repository) ProjectHeadNoReplace(ctx context.Context, tx pgx.Tx, docID string, versionID, snapshotID int64, revisionNo int, sha string) (bool, error) {
if tx == nil || docID == "" {
return false, fmt.Errorf("docgov: project head requires tx and doc_id")
}
tag, err := tx.Exec(ctx, `
UPDATE document_records
SET ledger_version_id=$2, head_snapshot_id=$3, head_revision_no=$4,
row_revision=COALESCE(row_revision,0)+1,
content_sha256=COALESCE(NULLIF($5,''), content_sha256),
updated_at=now()
WHERE doc_id=$1
AND COALESCE(head_revision_no, 0) < $4
`, docID, versionID, snapshotID, revisionNo, sha)
if err != nil {
return false, fmt.Errorf("docgov: project head no-replace: %w", err)
}
return tag.RowsAffected() == 1, nil
}