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.
137 lines
4.7 KiB
Go
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
|
|
}
|