Files

135 lines
3.9 KiB
Go

package docrevision
import (
"context"
"strings"
"sync"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// Service is the document revision ledger.
type Service struct {
pool *pgxpool.Pool
mu sync.Mutex
// docLocks 按 doc_id 分锁(T20 B3);hydrate/commit 只阻塞同文档。
docLocks *docLockIndex
blobs map[string]*ContentBlob
snapshots map[int64]*Snapshot
versions map[int64]*Version
events map[int64]*ChangeEvent
candidates map[int64]*AgentCandidate
reports map[int64]*ChangeReport
roots map[string]*DocRootPointers
idemp map[string]int64 // idempotency_key -> event id
projections map[string]ContentProjection
nextSnapID int64
nextVerID int64
nextEvtID int64
nextCandID int64
nextRepID int64
// Outbox hooks (Wave 5); optional.
OnOutbox func(ctx context.Context, eventType string, payload map[string]any) error
// OnFinalArtifact is invoked after a terminal ledger persist (agent_commit / version_sealed).
OnFinalArtifact func(ctx context.Context, kind, docID string, projectID, snapshotID int64, eventID string, payload []byte, sealedAt *time.Time)
// OnFinalArtifactTX writes FAV staging/outbox inside persistAgentCommitLocked.
OnFinalArtifactTX func(ctx context.Context, tx pgx.Tx, kind, docID string, projectID, snapshotID int64, eventID string, payload []byte, sealedAt *time.Time) error
// ImpactResolver returns deterministic downstream impacts (lineage); optional.
ImpactResolver func(ctx context.Context, docID string) []DocImpact
// CoverageResolver classifies trace-index coverage for a formal commit.
// When unset, reports stay index_pending and must not claim no impact.
CoverageResolver func(ctx context.Context, docID string) string
// NarrativeFetcher optionally loads typed PI narrative JSON when
// PI_CHANGE_REPORT_NARRATIVE is on and ReportJSON is empty.
NarrativeFetcher func(ctx context.Context, spec NarrativeFetchSpec) (string, error)
// Root pointer projection (controlled→docgov, freeform→freedoc); set by composition root.
rootPointers RootPointerProjector
// Deliverable I/O (path owner via workspace.DeliverableKey); set by composition root.
deliverables DeliverableStore
// DataDir is the environment data root used to resolve relative WorkDir paths.
DataDir string
// StoredDataRoot / HostDataRoot rewrite container paths (e.g. /app/data)
// to a host data root for CLI live mode. Empty means no rewrite.
StoredDataRoot string
HostDataRoot string
// Feature flags
LedgerWrite bool
LedgerRead bool
AgentCommit bool
DiffUI bool
Notify bool
}
func NewService(pool *pgxpool.Pool) *Service {
return &Service{
pool: pool,
docLocks: newDocLockIndex(),
blobs: map[string]*ContentBlob{},
snapshots: map[int64]*Snapshot{},
versions: map[int64]*Version{},
events: map[int64]*ChangeEvent{},
candidates: map[int64]*AgentCandidate{},
reports: map[int64]*ChangeReport{},
roots: map[string]*DocRootPointers{},
idemp: map[string]int64{},
projections: map[string]ContentProjection{},
LedgerWrite: true,
LedgerRead: true,
AgentCommit: true,
DiffUI: true,
Notify: true,
}
}
func (s *Service) EnsureRoot(docID, kind string, projectID int64, lifecycle string) *DocRootPointers {
g := s.lockDoc(docID)
defer g.Release()
s.mu.Lock()
defer s.mu.Unlock()
if r, ok := s.roots[docID]; ok {
return r
}
r := &DocRootPointers{
DocID: docID,
Kind: kind,
LifecycleState: lifecycle,
ProjectID: projectID,
RevisionSchemaVer: SchemaVersion,
}
s.roots[docID] = r
return r
}
func requireDocID(docID string) error {
if strings.TrimSpace(docID) == "" {
return ErrInvalidDocID
}
return nil
}
// NarrativeFetchSpec is the immutable input the PI narrator may see.
type NarrativeFetchSpec struct {
FromSHA string
ToSHA string
RuleRisk string
InputDigest string
Impacts []DocImpact
Hunks []DiffHunk
}