Wire intent→coordinator DocResolver confirm cards→action_executor kinds for comment, reopen, pending-supplements query, and real submit-review execution.
344 lines
9.0 KiB
Go
344 lines
9.0 KiB
Go
package traceimpact
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
// Store persists trace rows. Implementations must not delete decision history.
|
|
type Store interface {
|
|
UpsertItem(ctx context.Context, item Item) (Item, error)
|
|
ListItems(ctx context.Context, projectID int64) ([]Item, error)
|
|
UpsertEdge(ctx context.Context, edge Edge) (Edge, error)
|
|
ListEdges(ctx context.Context, projectID int64) ([]Edge, error)
|
|
PutIndex(ctx context.Context, st IndexState) error
|
|
IndexStatus(ctx context.Context, projectID int64, docID string) (IndexState, bool, error)
|
|
ListIndex(ctx context.Context, projectID int64) ([]IndexState, error)
|
|
SetEdgeReview(ctx context.Context, edgeID int64, review string) error
|
|
AddDecision(ctx context.Context, d Decision) (Decision, error)
|
|
ListDecisions(ctx context.Context, projectID int64) ([]Decision, error)
|
|
PutReopen(ctx context.Context, req ReopenRequest) (ReopenRequest, error)
|
|
GetReopen(ctx context.Context, id int64) (ReopenRequest, error)
|
|
ListPendingReopens(ctx context.Context, projectID int64) ([]ReopenRequest, error)
|
|
PutMark(ctx context.Context, m RecheckMark) (RecheckMark, error)
|
|
ListOpenMarks(ctx context.Context, projectID int64, stageKey string) ([]RecheckMark, error)
|
|
AckMark(ctx context.Context, id, userID int64) error
|
|
SetMarkStatus(ctx context.Context, id int64, status, reason string) error
|
|
GetEdge(ctx context.Context, edgeID int64) (Edge, error)
|
|
AppendRevision(ctx context.Context, rev ItemRevision) error
|
|
RevisionCount(ctx context.Context, itemID int64) (int, error)
|
|
AddProposal(ctx context.Context, projectID, userID int64, text string) (int64, error)
|
|
}
|
|
|
|
type memStore struct {
|
|
mu sync.Mutex
|
|
items map[int64]Item
|
|
byKey map[string]int64
|
|
edges map[int64]Edge
|
|
byEdge map[string]int64
|
|
index map[string]IndexState
|
|
decisions []Decision
|
|
reopens map[int64]ReopenRequest
|
|
marks map[int64]RecheckMark
|
|
revs map[int64][]ItemRevision
|
|
props []proposalRow
|
|
nextItem int64
|
|
nextEdge int64
|
|
nextDec int64
|
|
nextReq int64
|
|
nextMark int64
|
|
nextProp int64
|
|
}
|
|
|
|
func newMemStore() *memStore {
|
|
return &memStore{
|
|
items: map[int64]Item{},
|
|
byKey: map[string]int64{},
|
|
edges: map[int64]Edge{},
|
|
byEdge: map[string]int64{},
|
|
index: map[string]IndexState{},
|
|
reopens: map[int64]ReopenRequest{},
|
|
marks: map[int64]RecheckMark{},
|
|
}
|
|
}
|
|
|
|
func itemKey(projectID int64, kind, business string) string {
|
|
return fmt.Sprintf("%d|%s|%s", projectID, kind, business)
|
|
}
|
|
|
|
type proposalRow struct {
|
|
id int64
|
|
projectID int64
|
|
userID int64
|
|
text string
|
|
}
|
|
|
|
func (m *memStore) UpsertItem(_ context.Context, item Item) (Item, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
k := itemKey(item.ProjectID, item.Kind, item.BusinessKey)
|
|
if id, ok := m.byKey[k]; ok {
|
|
cur := m.items[id]
|
|
if item.Statement != "" {
|
|
cur.Statement = item.Statement
|
|
}
|
|
if item.SourceDocID != "" {
|
|
cur.SourceDocID = item.SourceDocID
|
|
}
|
|
cur.ReviewState = item.ReviewState
|
|
cur.Tombstone = item.Tombstone
|
|
m.items[id] = cur
|
|
return cur, nil
|
|
}
|
|
m.nextItem++
|
|
item.ID = m.nextItem
|
|
if item.Lifecycle == "" {
|
|
item.Lifecycle = "active"
|
|
}
|
|
m.items[item.ID] = item
|
|
m.byKey[k] = item.ID
|
|
return item, nil
|
|
}
|
|
|
|
func (m *memStore) ListItems(_ context.Context, projectID int64) ([]Item, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []Item
|
|
for _, it := range m.items {
|
|
if it.ProjectID == projectID {
|
|
out = append(out, it)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *memStore) UpsertEdge(_ context.Context, edge Edge) (Edge, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
k := fmt.Sprintf("%d|%d|%d|%s|%s", edge.ProjectID, edge.FromItemID, edge.ToItemID, edge.RelationType, edge.Provenance)
|
|
if id, ok := m.byEdge[k]; ok {
|
|
cur := m.edges[id]
|
|
if cur.ReviewState == ReviewRejected && edge.ReviewState == ReviewCandidate {
|
|
return cur, nil
|
|
}
|
|
if edge.ReviewState != "" && cur.ReviewState != ReviewConfirmed {
|
|
cur.ReviewState = edge.ReviewState
|
|
}
|
|
m.edges[id] = cur
|
|
return cur, nil
|
|
}
|
|
m.nextEdge++
|
|
edge.ID = m.nextEdge
|
|
m.edges[edge.ID] = edge
|
|
m.byEdge[k] = edge.ID
|
|
return edge, nil
|
|
}
|
|
|
|
func (m *memStore) ListEdges(_ context.Context, projectID int64) ([]Edge, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []Edge
|
|
for _, e := range m.edges {
|
|
if e.ProjectID == projectID {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *memStore) PutIndex(_ context.Context, st IndexState) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if st.Extractor == "" {
|
|
st.Extractor = ExtractorVersion
|
|
}
|
|
m.index[fmt.Sprintf("%d|%s", st.ProjectID, st.DocID)] = st
|
|
return nil
|
|
}
|
|
|
|
func (m *memStore) IndexStatus(_ context.Context, projectID int64, docID string) (IndexState, bool, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
st, ok := m.index[fmt.Sprintf("%d|%s", projectID, docID)]
|
|
return st, ok, nil
|
|
}
|
|
|
|
func (m *memStore) ListIndex(_ context.Context, projectID int64) ([]IndexState, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []IndexState
|
|
for _, st := range m.index {
|
|
if st.ProjectID == projectID {
|
|
out = append(out, st)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *memStore) SetEdgeReview(_ context.Context, edgeID int64, review string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.edges[edgeID]
|
|
if !ok {
|
|
return fmt.Errorf("traceimpact: edge not found")
|
|
}
|
|
e.ReviewState = review
|
|
m.edges[edgeID] = e
|
|
return nil
|
|
}
|
|
|
|
func (m *memStore) AddDecision(_ context.Context, d Decision) (Decision, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.nextDec++
|
|
d.ID = m.nextDec
|
|
m.decisions = append(m.decisions, d)
|
|
return d, nil
|
|
}
|
|
|
|
func (m *memStore) ListDecisions(_ context.Context, projectID int64) ([]Decision, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []Decision
|
|
for _, d := range m.decisions {
|
|
if d.ProjectID == projectID {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *memStore) PutReopen(_ context.Context, req ReopenRequest) (ReopenRequest, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if req.ID == 0 {
|
|
m.nextReq++
|
|
req.ID = m.nextReq
|
|
}
|
|
m.reopens[req.ID] = req
|
|
return req, nil
|
|
}
|
|
|
|
func (m *memStore) GetReopen(_ context.Context, id int64) (ReopenRequest, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
req, ok := m.reopens[id]
|
|
if !ok {
|
|
return ReopenRequest{}, fmt.Errorf("traceimpact: reopen request not found")
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func (m *memStore) ListPendingReopens(_ context.Context, projectID int64) ([]ReopenRequest, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []ReopenRequest
|
|
for _, req := range m.reopens {
|
|
if req.ProjectID == projectID && req.Status == ReopenPending {
|
|
out = append(out, req)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *memStore) PutMark(_ context.Context, mark RecheckMark) (RecheckMark, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for _, existing := range m.marks {
|
|
if existing.ProjectID == mark.ProjectID && existing.StageKey == mark.StageKey && existing.SourceDoc == mark.SourceDoc && existing.Status == MarkOpen {
|
|
return existing, nil
|
|
}
|
|
}
|
|
m.nextMark++
|
|
mark.ID = m.nextMark
|
|
if mark.Status == "" {
|
|
mark.Status = MarkOpen
|
|
}
|
|
m.marks[mark.ID] = mark
|
|
return mark, nil
|
|
}
|
|
|
|
func (m *memStore) ListOpenMarks(_ context.Context, projectID int64, stageKey string) ([]RecheckMark, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []RecheckMark
|
|
for _, mark := range m.marks {
|
|
if mark.ProjectID != projectID || mark.Status != MarkOpen {
|
|
continue
|
|
}
|
|
if stageKey != "" && mark.StageKey != stageKey {
|
|
continue
|
|
}
|
|
out = append(out, mark)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *memStore) AckMark(_ context.Context, id, userID int64) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
mark, ok := m.marks[id]
|
|
if !ok {
|
|
return fmt.Errorf("traceimpact: mark not found")
|
|
}
|
|
mark.AckedBy = userID
|
|
m.marks[id] = mark
|
|
return nil
|
|
}
|
|
|
|
func (m *memStore) SetMarkStatus(_ context.Context, id int64, status, reason string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
mark, ok := m.marks[id]
|
|
if !ok {
|
|
return fmt.Errorf("traceimpact: mark not found")
|
|
}
|
|
mark.Status = status
|
|
if reason != "" {
|
|
mark.Reason = reason
|
|
}
|
|
m.marks[id] = mark
|
|
return nil
|
|
}
|
|
|
|
func (m *memStore) GetEdge(_ context.Context, edgeID int64) (Edge, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
e, ok := m.edges[edgeID]
|
|
if !ok {
|
|
return Edge{}, fmt.Errorf("traceimpact: edge not found")
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (m *memStore) AppendRevision(_ context.Context, rev ItemRevision) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.revs == nil {
|
|
m.revs = map[int64][]ItemRevision{}
|
|
}
|
|
for _, existing := range m.revs[rev.ItemID] {
|
|
if existing.SnapshotID == rev.SnapshotID && existing.ContentHash == rev.ContentHash && existing.Extractor == rev.Extractor {
|
|
return nil
|
|
}
|
|
}
|
|
rev.RevisionNo = len(m.revs[rev.ItemID]) + 1
|
|
m.revs[rev.ItemID] = append(m.revs[rev.ItemID], rev)
|
|
return nil
|
|
}
|
|
|
|
func (m *memStore) RevisionCount(_ context.Context, itemID int64) (int, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return len(m.revs[itemID]), nil
|
|
}
|
|
|
|
func (m *memStore) AddProposal(_ context.Context, projectID, userID int64, text string) (int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.nextProp++
|
|
m.props = append(m.props, proposalRow{id: m.nextProp, projectID: projectID, userID: userID, text: text})
|
|
return m.nextProp, nil
|
|
}
|