A candidate used the same report later attached to the version, so a preview could be treated as the formal result. Proposed reports now close on replace or reject, and only the commit writes an authoritative successor.
157 lines
4.2 KiB
Go
157 lines
4.2 KiB
Go
package docrevision
|
|
|
|
// AllowsSuggestedActions is true only for the report a user may still act on.
|
|
// A proposed estimate stops being actionable once it is superseded, rejected,
|
|
// expired, or replaced by an authoritative successor. The successor itself remains actionable.
|
|
func (r *ChangeReport) AllowsSuggestedActions() bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
switch {
|
|
case r.Authority == ReportProposed && r.Lifecycle == ReportActive:
|
|
return true
|
|
case r.Authority == ReportAuthoritative && r.Lifecycle == ReportCommitted:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func stampReportDisposition(rep *ChangeReport) {
|
|
if rep == nil {
|
|
return
|
|
}
|
|
if rep.DeterministicFacts == nil {
|
|
rep.DeterministicFacts = map[string]any{}
|
|
}
|
|
if rep.Authority != "" {
|
|
rep.DeterministicFacts["report_authority"] = rep.Authority
|
|
}
|
|
if rep.Lifecycle != "" {
|
|
rep.DeterministicFacts["report_lifecycle"] = rep.Lifecycle
|
|
}
|
|
if rep.SuccessorReportID != nil {
|
|
rep.DeterministicFacts["successor_report_id"] = *rep.SuccessorReportID
|
|
}
|
|
}
|
|
|
|
func liftReportDisposition(rep *ChangeReport) {
|
|
if rep == nil || rep.DeterministicFacts == nil {
|
|
return
|
|
}
|
|
if rep.Authority == "" {
|
|
if a, ok := rep.DeterministicFacts["report_authority"].(string); ok {
|
|
rep.Authority = a
|
|
}
|
|
}
|
|
if rep.Lifecycle == "" {
|
|
if life, ok := rep.DeterministicFacts["report_lifecycle"].(string); ok {
|
|
rep.Lifecycle = life
|
|
}
|
|
}
|
|
if rep.SuccessorReportID == nil {
|
|
if id, ok := factInt64(rep.DeterministicFacts["successor_report_id"]); ok {
|
|
rep.SuccessorReportID = &id
|
|
}
|
|
}
|
|
}
|
|
|
|
func factInt64(v any) (int64, bool) {
|
|
switch n := v.(type) {
|
|
case int64:
|
|
return n, true
|
|
case int:
|
|
return int64(n), true
|
|
case float64:
|
|
return int64(n), true
|
|
case jsonNumber:
|
|
i, err := n.Int64()
|
|
return i, err == nil
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
// jsonNumber is the subset of encoding/json.Number used after a UseNumber decode.
|
|
type jsonNumber interface {
|
|
Int64() (int64, error)
|
|
}
|
|
|
|
func cloneChangeReport(src *ChangeReport) *ChangeReport {
|
|
if src == nil {
|
|
return nil
|
|
}
|
|
cp := *src
|
|
cp.ID = 0
|
|
cp.SuccessorReportID = nil
|
|
cp.Summary = append([]string(nil), src.Summary...)
|
|
cp.RiskItems = append([]RiskItem(nil), src.RiskItems...)
|
|
cp.DocumentImpacts = append([]DocImpact(nil), src.DocumentImpacts...)
|
|
cp.OpenQuestions = append([]string(nil), src.OpenQuestions...)
|
|
cp.DeterministicFacts = map[string]any{}
|
|
for k, v := range src.DeterministicFacts {
|
|
cp.DeterministicFacts[k] = v
|
|
}
|
|
if src.ProjectImpacts != nil {
|
|
cp.ProjectImpacts = map[string]any{}
|
|
for k, v := range src.ProjectImpacts {
|
|
cp.ProjectImpacts[k] = v
|
|
}
|
|
}
|
|
return &cp
|
|
}
|
|
|
|
func (s *Service) assignReportIDLocked(rep *ChangeReport) {
|
|
rep.ID = nextFreeID(&s.nextRepID, func(id int64) bool { return s.reports[id] != nil })
|
|
s.reports[rep.ID] = rep
|
|
}
|
|
|
|
// supersedeActiveProposedLocked closes earlier estimates for the same document.
|
|
// The new candidate is the only active proposed report.
|
|
func (s *Service) supersedeActiveProposedLocked(docID string) []*ChangeReport {
|
|
var out []*ChangeReport
|
|
for _, rep := range s.reports {
|
|
if rep == nil || rep.DocID != docID || rep.Authority != ReportProposed || rep.Lifecycle != ReportActive {
|
|
continue
|
|
}
|
|
rep.Lifecycle = ReportSuperseded
|
|
stampReportDisposition(rep)
|
|
out = append(out, rep)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Service) sealProposedWithSuccessorLocked(proposed *ChangeReport) *ChangeReport {
|
|
auth := cloneChangeReport(proposed)
|
|
auth.Authority = ReportAuthoritative
|
|
auth.Lifecycle = ReportCommitted
|
|
auth.SuccessorReportID = nil
|
|
auth.CreatedAt = proposed.CreatedAt
|
|
s.assignReportIDLocked(auth)
|
|
proposed.Lifecycle = ReportCommitted
|
|
proposed.SuccessorReportID = &auth.ID
|
|
stampReportDisposition(proposed)
|
|
stampReportDisposition(auth)
|
|
return auth
|
|
}
|
|
|
|
func (s *Service) closeProposedLocked(rep *ChangeReport, lifecycle string) {
|
|
if rep == nil || rep.Authority != ReportProposed || rep.Lifecycle != ReportActive {
|
|
return
|
|
}
|
|
rep.Lifecycle = lifecycle
|
|
stampReportDisposition(rep)
|
|
}
|
|
|
|
func (s *Service) expireOtherProposedLocked(docID string, keep int64) {
|
|
for _, rep := range s.reports {
|
|
if rep == nil || rep.ID == keep || rep.DocID != docID {
|
|
continue
|
|
}
|
|
if rep.Authority == ReportProposed && rep.Lifecycle == ReportActive {
|
|
rep.Lifecycle = ReportExpired
|
|
stampReportDisposition(rep)
|
|
}
|
|
}
|
|
}
|