package docquery import ( "context" "errors" "os" "strings" "chat-agent-system/internal/docgov" "chat-agent-system/internal/docrevision" ) const ( ConsistencyAligned = "aligned" ConsistencyMissing = "missing" ConsistencyDrift = "drift" ConsistencyEmpty = "empty" ConsistencyLedgerMissing = "ledger_missing" MaxBatchGet = 100 ) var ( ErrClosed = errors.New("docquery: authorization failed") ErrBatchLimit = errors.New("docquery: batch exceeds 100") ErrCrossProject = errors.New("docquery: mixed project references") ErrLedgerMissing = errors.New("docquery: ledger missing") ErrFavNotEnabled = errors.New("docquery: FAV degraded read is disabled") ) // Principal comes only from the authentication adapter. It must not carry // caller-supplied role or duty overrides. type Principal struct { UserID int64 } // DocRef identifies a document. User/role/duty must not be attached. type DocRef struct { DocID string } type ContentView struct { DocID string `json:"doc_id"` Version int `json:"version"` ContentSHA256 string `json:"content_sha256"` Markdown string `json:"markdown"` IsTemplateOnly bool `json:"is_template_only"` Consistency string `json:"consistency"` RecordSHA256 string `json:"record_sha256,omitempty"` FileSHA256 string `json:"file_sha256,omitempty"` HeadSHA256 string `json:"head_sha256,omitempty"` } type Authorizer func(ctx context.Context, p Principal, rec *docgov.DocumentRecord) error type Service struct { Gov *docgov.GovernanceEngine Rev *docrevision.Service RepoRoot string Authorize Authorizer FavDegradedRead bool Vault VaultReader } type VaultReader interface { ReadVerified(ctx context.Context, projectID int64, logicalID string) (markdown string, sha string, err error) } func FavDegradedReadEnabled() bool { switch strings.ToLower(strings.TrimSpace(os.Getenv("FAV_DEGRADED_READ"))) { case "1", "true", "yes": return true default: return false } } func (s *Service) Get(ctx context.Context, p Principal, ref DocRef) (*ContentView, error) { if ctx == nil { ctx = context.Background() } if err := ctx.Err(); err != nil { return nil, ErrClosed } if p.UserID == 0 { return nil, ErrClosed } if s == nil || s.Gov == nil { return nil, ErrClosed } rec, err := s.Gov.GetDocument(ctx, ref.DocID) if err != nil || rec == nil { return nil, ErrClosed } if s.Authorize != nil { if err := s.Authorize(ctx, p, rec); err != nil { return nil, ErrClosed } } if s.Rev != nil { md, sha, herr := s.Rev.HeadContent(ctx, ref.DocID) if herr == nil && md != "" { return s.viewFromLedger(ctx, rec, md, sha), nil } } if s.FavDegradedRead && s.Vault != nil { md, sha, verr := s.Vault.ReadVerified(ctx, rec.ProjectID, rec.DocID) if verr != nil { return nil, verr } return s.viewFromLedger(ctx, rec, md, sha), nil } if s.FavDegradedRead { return nil, ErrFavNotEnabled } return &ContentView{ DocID: rec.DocID, Version: rec.Version, IsTemplateOnly: rec.IsTemplateOnly, Consistency: ConsistencyLedgerMissing, RecordSHA256: rec.ContentSHA256, }, ErrLedgerMissing } func (s *Service) BatchGet(ctx context.Context, p Principal, refs []DocRef) ([]*ContentView, error) { if len(refs) > MaxBatchGet { return nil, ErrBatchLimit } if p.UserID == 0 { return nil, ErrClosed } var projectID int64 seen := map[string]bool{} out := make([]*ContentView, 0, len(refs)) for _, ref := range refs { if seen[ref.DocID] { continue } seen[ref.DocID] = true rec, err := s.Gov.GetDocument(ctx, ref.DocID) if err != nil || rec == nil { return nil, ErrClosed } if projectID == 0 { projectID = rec.ProjectID } else if rec.ProjectID != projectID { return nil, ErrCrossProject } view, err := s.Get(ctx, p, ref) if err != nil { return nil, err } out = append(out, view) } return out, nil } func (s *Service) viewFromLedger(ctx context.Context, rec *docgov.DocumentRecord, md, sha string) *ContentView { view := &ContentView{ DocID: rec.DocID, Version: rec.Version, ContentSHA256: sha, Markdown: md, IsTemplateOnly: rec.IsTemplateOnly, Consistency: ConsistencyAligned, RecordSHA256: rec.ContentSHA256, HeadSHA256: sha, } if s.Rev == nil { return view } class, err := s.Rev.InspectProjection(ctx, rec.DocID, s.RepoRoot) if err != nil { view.Consistency = ConsistencyMissing return view } view.FileSHA256 = class.RawSHA switch class.Class { case "", docrevision.ProjectionAligned, docrevision.ProjectionNoLedger: view.Consistency = ConsistencyAligned case docrevision.ProjectionMissing: view.Consistency = ConsistencyMissing case docrevision.ProjectionEmpty: view.Consistency = ConsistencyEmpty default: view.Consistency = ConsistencyDrift } return view } func (v *ContentView) ToDocumentContent() *docgov.DocumentContent { if v == nil { return nil } return &docgov.DocumentContent{ DocID: v.DocID, Version: v.Version, ContentSHA256: v.ContentSHA256, Markdown: v.Markdown, IsTemplateOnly: v.IsTemplateOnly, Consistency: v.Consistency, RecordSHA256: v.RecordSHA256, FileSHA256: v.FileSHA256, } }