From 77fa5af3ac80e71c6e2a71cbe79be630da337204 Mon Sep 17 00:00:00 2001 From: Samuel Lucidi Date: Thu, 19 Oct 2023 14:22:29 -0400 Subject: [PATCH] :sparkles: Add `confidence` and `risk` to Application and Archetype resources. (#530) * Add `confidence` and `risk` fields to Application and Archetype resources. Overall confidence calculated using the algorithm from Pathfinder for each assessment, then summed and divided by total number of assessments. Overall risk level is `green` if all assessments are `green`, otherwise the overall risk is `red`, `unknown`, or `yellow` in that priority order if any assessments have that risk level. * Moved some assessment status logic out of the resource and into the assessment package where it belongs. * Added ArchetypeResolver to better encapsulate the related behaviors and for symmetry with ApplicationResolver. * Simplified the Application and Archetype endpoints by replacing several related resource `With*` methods with `WithResolver`. * Improved consistency of the APIs within the `assessment` package. --------- Signed-off-by: Sam Lucidi --- api/application.go | 107 ++++++++----------- api/archetype.go | 57 +++++------ api/assessment.go | 83 ++------------- assessment/application.go | 82 +++++++++++++-- assessment/archetype.go | 73 +++++++++++-- assessment/{section.go => assessment.go} | 124 +++++++++++++++++++++++ assessment/membership.go | 35 ++++--- assessment/pkg.go | 81 +++++++++------ assessment/questionnaire.go | 7 +- assessment/tag.go | 7 +- 10 files changed, 416 insertions(+), 240 deletions(-) rename assessment/{section.go => assessment.go} (56%) diff --git a/api/application.go b/api/application.go index 2ef87ef0..d42a0c03 100644 --- a/api/application.go +++ b/api/application.go @@ -134,28 +134,13 @@ func (h ApplicationHandler) Get(ctx *gin.Context) { return } resolver := assessment.NewApplicationResolver(m, tagsResolver, membership, questionnaire) - archetypes, err := resolver.Archetypes() - if err != nil { - _ = ctx.Error(err) - return - } - archetypeTags, err := resolver.ArchetypeTags() - if err != nil { - _ = ctx.Error(err) - return - } - r := Application{} r.With(m, tags) - r.WithArchetypes(archetypes) - r.WithVirtualTags(archetypeTags, SourceArchetype) - r.WithVirtualTags(resolver.AssessmentTags(), SourceAssessment) - r.Assessed, err = resolver.Assessed() + err = r.WithResolver(resolver) if err != nil { _ = ctx.Error(err) return } - h.Respond(ctx, http.StatusOK, r) } @@ -198,22 +183,9 @@ func (h ApplicationHandler) List(ctx *gin.Context) { return } resolver := assessment.NewApplicationResolver(&list[i], tagsResolver, membership, questionnaire) - archetypes, aErr := resolver.Archetypes() - if aErr != nil { - _ = ctx.Error(aErr) - return - } - archetypeTags, aErr := resolver.ArchetypeTags() - if aErr != nil { - _ = ctx.Error(aErr) - return - } r := Application{} r.With(&list[i], tags) - r.WithArchetypes(archetypes) - r.WithVirtualTags(archetypeTags, SourceArchetype) - r.WithVirtualTags(resolver.AssessmentTags(), SourceAssessment) - r.Assessed, err = resolver.Assessed() + err = r.WithResolver(resolver) if err != nil { _ = ctx.Error(err) return @@ -274,27 +246,12 @@ func (h ApplicationHandler) Create(ctx *gin.Context) { return } resolver := assessment.NewApplicationResolver(m, tagsResolver, membership, questionnaire) - archetypes, err := resolver.Archetypes() - if err != nil { - _ = ctx.Error(err) - return - } - archetypeTags, err := resolver.ArchetypeTags() - if err != nil { - _ = ctx.Error(err) - return - } - r.With(m, tags) - r.WithArchetypes(archetypes) - r.WithVirtualTags(archetypeTags, SourceArchetype) - r.WithVirtualTags(resolver.AssessmentTags(), SourceArchetype) - r.Assessed, err = resolver.Assessed() + err = r.WithResolver(resolver) if err != nil { _ = ctx.Error(err) return } - h.Respond(ctx, http.StatusCreated, r) } @@ -1018,12 +975,12 @@ func (h ApplicationHandler) AssessmentList(ctx *gin.Context) { _ = ctx.Error(err) return } - assessments := m.Assessments - for _, a := range archetypes { - assessments = append(assessments, a.Assessments...) + for _, arch := range archetypes { + for _, a := range arch.Assessments { + assessments = append(assessments, *a.Assessment) + } } - resources := []Assessment{} for i := range assessments { r := Assessment{} @@ -1112,13 +1069,15 @@ type Application struct { Comments string `json:"comments"` Identities []Ref `json:"identities"` Tags []TagRef `json:"tags"` - BusinessService *Ref `json:"businessService"` + BusinessService *Ref `json:"businessService" yaml:"businessService"` Owner *Ref `json:"owner"` Contributors []Ref `json:"contributors"` - MigrationWave *Ref `json:"migrationWave"` + MigrationWave *Ref `json:"migrationWave" yaml:"migrationWave"` Archetypes []Ref `json:"archetypes"` Assessments []Ref `json:"assessments"` Assessed bool `json:"assessed"` + Risk string `json:"risk"` + Confidence int `json:"confidence"` } // @@ -1168,16 +1127,6 @@ func (r *Application) With(m *model.Application, tags []model.ApplicationTag) { } } -// -// WithArchetypes updates the resource with archetypes. -func (r *Application) WithArchetypes(archetypes []model.Archetype) { - for _, a := range archetypes { - ref := Ref{} - ref.With(a.ID, a.Name) - r.Archetypes = append(r.Archetypes, ref) - } -} - // // WithVirtualTags updates the resource with tags derived from assessments. func (r *Application) WithVirtualTags(tags []model.Tag, source string) { @@ -1188,6 +1137,40 @@ func (r *Application) WithVirtualTags(tags []model.Tag, source string) { } } +// +// WithResolver uses an ApplicationResolver to update the resource with +// values derived from the application's assessments and archetypes. +func (r *Application) WithResolver(resolver *assessment.ApplicationResolver) (err error) { + archetypes, err := resolver.Archetypes() + if err != nil { + return + } + for _, a := range archetypes { + ref := Ref{} + ref.With(a.ID, a.Name) + r.Archetypes = append(r.Archetypes, ref) + } + archetypeTags, err := resolver.ArchetypeTags() + if err != nil { + return + } + r.WithVirtualTags(archetypeTags, SourceArchetype) + r.WithVirtualTags(resolver.AssessmentTags(), SourceAssessment) + r.Assessed, err = resolver.Assessed() + if err != nil { + return + } + r.Confidence, err = resolver.Confidence() + if err != nil { + return + } + r.Risk, err = resolver.Risk() + if err != nil { + return + } + return +} + // // Model builds a model. func (r *Application) Model() (m *model.Application) { diff --git a/api/archetype.go b/api/archetype.go index 06f0c935..eda38969 100644 --- a/api/archetype.go +++ b/api/archetype.go @@ -57,32 +57,24 @@ func (h ArchetypeHandler) Get(ctx *gin.Context) { _ = ctx.Error(result.Error) return } - membership := assessment.NewMembershipResolver(h.DB(ctx)) - applications, err := membership.Applications(m) - if err != nil { - _ = ctx.Error(err) - return - } - questionnaires, err := assessment.NewQuestionnaireResolver(h.DB(ctx)) if err != nil { _ = ctx.Error(err) return } - tags, err := assessment.NewTagResolver(h.DB(ctx)) if err != nil { _ = ctx.Error(err) } - - resolver := assessment.NewArchetypeResolver(m, tags) - + resolver := assessment.NewArchetypeResolver(m, tags, membership, questionnaires) r := Archetype{} r.With(m) - r.WithApplications(applications) - r.WithAssessmentTags(resolver.AssessmentTags()) - r.Assessed = questionnaires.Assessed(m.Assessments) + err = r.WithResolver(resolver) + if err != nil { + _ = ctx.Error(err) + return + } h.Respond(ctx, http.StatusOK, r) } @@ -101,7 +93,6 @@ func (h ArchetypeHandler) List(ctx *gin.Context) { _ = ctx.Error(result.Error) return } - questionnaires, err := assessment.NewQuestionnaireResolver(h.DB(ctx)) if err != nil { _ = ctx.Error(err) @@ -111,22 +102,18 @@ func (h ArchetypeHandler) List(ctx *gin.Context) { if err != nil { _ = ctx.Error(err) } - membership := assessment.NewMembershipResolver(h.DB(ctx)) resources := []Archetype{} for i := range list { m := &list[i] + resolver := assessment.NewArchetypeResolver(m, tags, membership, questionnaires) r := Archetype{} - applications, err := membership.Applications(m) + r.With(m) + err = r.WithResolver(resolver) if err != nil { _ = ctx.Error(err) return } - resolver := assessment.NewArchetypeResolver(m, tags) - r.With(m) - r.WithApplications(applications) - r.WithAssessmentTags(resolver.AssessmentTags()) - r.Assessed = questionnaires.Assessed(m.Assessments) resources = append(resources, r) } @@ -165,14 +152,13 @@ func (h ArchetypeHandler) Create(ctx *gin.Context) { } membership := assessment.NewMembershipResolver(h.DB(ctx)) - applications, err := membership.Applications(m) + resolver := assessment.NewArchetypeResolver(m, nil, membership, nil) + r.With(m) + err = r.WithResolver(resolver) if err != nil { _ = ctx.Error(err) return } - - r.With(m) - r.WithApplications(applications) h.Respond(ctx, http.StatusCreated, r) } @@ -356,6 +342,8 @@ type Archetype struct { Applications []Ref `json:"applications" yaml:"applications"` Assessments []Ref `json:"assessments" yaml:"assessments"` Assessed bool `json:"assessed"` + Risk string `json:"risk"` + Confidence int `json:"confidence"` Review *Ref `json:"review"` } @@ -398,23 +386,24 @@ func (r *Archetype) With(m *model.Archetype) { } // -// WithApplications updates the Archetype resource with the applications. -func (r *Archetype) WithApplications(apps []model.Application) { +// WithResolver uses an ArchetypeResolver to update the resource with +// values derived from the archetype's assessments. +func (r *Archetype) WithResolver(resolver *assessment.ArchetypeResolver) (err error) { + r.Risk = resolver.Risk() + r.Confidence = resolver.Confidence() + r.Assessed = resolver.Assessed() + apps, err := resolver.Applications() for i := range apps { ref := Ref{} ref.With(apps[i].ID, apps[i].Name) r.Applications = append(r.Applications, ref) } -} - -// -// WithAssessmentTags updates the Archetype resource with tags inherited from assessments. -func (r *Archetype) WithAssessmentTags(tags []model.Tag) { - for _, t := range tags { + for _, t := range resolver.AssessmentTags() { ref := TagRef{} ref.With(t.ID, t.Name, SourceAssessment, true) r.Tags = append(r.Tags, ref) } + return } // diff --git a/api/assessment.go b/api/assessment.go index b6bbc326..a8bc5d4c 100644 --- a/api/assessment.go +++ b/api/assessment.go @@ -16,23 +16,6 @@ const ( AssessmentRoot = AssessmentsRoot + "/:" + ID ) -// -// Assessment status -const ( - AssessmentEmpty = "empty" - AssessmentStarted = "started" - AssessmentComplete = "complete" -) - -// -// Assessment risks -const ( - RiskRed = "red" - RiskYellow = "yellow" - RiskGreen = "green" - RiskUnknown = "unknown" -) - // // AssessmentHandler handles Assessment resource routes. type AssessmentHandler struct { @@ -189,9 +172,6 @@ func (r *Assessment) With(m *model.Assessment) { r.Questionnaire = r.ref(m.QuestionnaireID, &m.Questionnaire) r.Archetype = r.refPtr(m.ArchetypeID, m.Archetype) r.Application = r.refPtr(m.ApplicationID, m.Application) - _ = json.Unmarshal(m.Sections, &r.Sections) - _ = json.Unmarshal(m.Thresholds, &r.Thresholds) - _ = json.Unmarshal(m.RiskMessages, &r.RiskMessages) r.Stakeholders = []Ref{} for _, s := range m.Stakeholders { ref := Ref{} @@ -204,15 +184,14 @@ func (r *Assessment) With(m *model.Assessment) { ref.With(sg.ID, sg.Name) r.StakeholderGroups = append(r.StakeholderGroups, ref) } - if r.Complete() { - r.Status = AssessmentComplete - } else if r.Started() { - r.Status = AssessmentStarted - } else { - r.Status = AssessmentEmpty - } - r.Risk = r.RiskLevel() - r.Confidence = assessment.Confidence(r.Sections) + a := assessment.Assessment{} + a.With(m) + r.Risk = a.Risk() + r.Confidence = a.Confidence() + r.RiskMessages = a.RiskMessages + r.Thresholds = a.Thresholds + r.Sections = a.Sections + r.Status = a.Status() } // @@ -246,49 +225,3 @@ func (r *Assessment) Model() (m *model.Assessment) { } return } - -func (r *Assessment) RiskLevel() string { - var total uint - colors := make(map[string]uint) - for _, s := range r.Sections { - for _, risk := range s.Risks() { - colors[risk]++ - total++ - } - } - if total == 0 { - return RiskGreen - } - if (float64(colors[RiskRed]) / float64(total)) >= (float64(r.Thresholds.Red) / float64(100)) { - return RiskRed - } - if (float64(colors[RiskYellow]) / float64(total)) >= (float64(r.Thresholds.Yellow) / float64(100)) { - return RiskYellow - } - if (float64(colors[RiskUnknown]) / float64(total)) >= (float64(r.Thresholds.Unknown) / float64(100)) { - return RiskUnknown - } - return RiskGreen -} - -// -// Complete returns whether all sections have been completed. -func (r *Assessment) Complete() bool { - for _, s := range r.Sections { - if !s.Complete() { - return false - } - } - return true -} - -// -// Started returns whether any sections have been started. -func (r *Assessment) Started() bool { - for _, s := range r.Sections { - if s.Started() { - return true - } - } - return false -} diff --git a/assessment/application.go b/assessment/application.go index 307cba29..1b7e7fd9 100644 --- a/assessment/application.go +++ b/assessment/application.go @@ -4,15 +4,36 @@ import ( "github.com/konveyor/tackle2-hub/model" ) +// +// Application represents an Application with its assessments. +type Application struct { + *model.Application + Assessments []Assessment +} + +// +// With updates the Application with the db model and deserializes its assessments. +func (r *Application) With(m *model.Application) { + r.Application = m + for _, a := range m.Assessments { + assessment := Assessment{} + assessment.With(&a) + r.Assessments = append(r.Assessments, assessment) + } +} + // // NewApplicationResolver creates a new ApplicationResolver from an application and other shared resolvers. -func NewApplicationResolver(app *model.Application, tags *TagResolver, membership *MembershipResolver, questionnaire *QuestionnaireResolver) (a *ApplicationResolver) { +func NewApplicationResolver(m *model.Application, tags *TagResolver, membership *MembershipResolver, questionnaire *QuestionnaireResolver) (a *ApplicationResolver) { a = &ApplicationResolver{ - application: app, tagResolver: tags, membershipResolver: membership, questionnaireResolver: questionnaire, } + app := Application{} + app.With(m) + a.application = app + return } @@ -20,8 +41,8 @@ func NewApplicationResolver(app *model.Application, tags *TagResolver, membershi // ApplicationResolver wraps an Application model // with archetype and assessment resolution behavior. type ApplicationResolver struct { - application *model.Application - archetypes []model.Archetype + application Application + archetypes []Archetype tagResolver *TagResolver membershipResolver *MembershipResolver questionnaireResolver *QuestionnaireResolver @@ -29,7 +50,7 @@ type ApplicationResolver struct { // // Archetypes returns the list of archetypes the application is a member of. -func (r *ApplicationResolver) Archetypes() (archetypes []model.Archetype, err error) { +func (r *ApplicationResolver) Archetypes() (archetypes []Archetype, err error) { if len(r.archetypes) > 0 { archetypes = r.archetypes return @@ -60,7 +81,7 @@ func (r *ApplicationResolver) ArchetypeTags() (tags []model.Tag, err error) { // inherit assessment tags from any of its archetypes. if len(r.application.Assessments) == 0 { for _, assessment := range a.Assessments { - aTags := r.tagResolver.Assessment(&assessment) + aTags := r.tagResolver.Assessment(assessment) for _, t := range aTags { if _, found := seenTags[t.ID]; !found { seenTags[t.ID] = true @@ -79,7 +100,7 @@ func (r *ApplicationResolver) ArchetypeTags() (tags []model.Tag, err error) { func (r *ApplicationResolver) AssessmentTags() (tags []model.Tag) { seenTags := make(map[uint]bool) for _, assessment := range r.application.Assessments { - aTags := r.tagResolver.Assessment(&assessment) + aTags := r.tagResolver.Assessment(assessment) for _, t := range aTags { if _, found := seenTags[t.ID]; !found { seenTags[t.ID] = true @@ -90,6 +111,46 @@ func (r *ApplicationResolver) AssessmentTags() (tags []model.Tag) { return } +// +// Risk returns the overall risk level for the application based on its or its archetypes' assessments. +func (r *ApplicationResolver) Risk() (risk string, err error) { + var assessments []Assessment + if len(r.application.Assessments) > 0 { + assessments = r.application.Assessments + } else { + archetypes, aErr := r.Archetypes() + if aErr != nil { + err = aErr + return + } + for _, a := range archetypes { + assessments = append(assessments, a.Assessments...) + } + } + risk = Risk(assessments) + return +} + +// +// Confidence returns the application's overall assessment confidence score. +func (r *ApplicationResolver) Confidence() (confidence int, err error) { + var assessments []Assessment + if len(r.application.Assessments) > 0 { + assessments = r.application.Assessments + } else { + archetypes, aErr := r.Archetypes() + if aErr != nil { + err = aErr + return + } + for _, a := range archetypes { + assessments = append(assessments, a.Assessments...) + } + } + confidence = Confidence(assessments) + return +} + // // Assessed returns whether the application has been fully assessed. func (r *ApplicationResolver) Assessed() (assessed bool, err error) { @@ -104,11 +165,12 @@ func (r *ApplicationResolver) Assessed() (assessed bool, err error) { if err != nil { return } + assessedCount := 0 for _, a := range archetypes { - if !r.questionnaireResolver.Assessed(a.Assessments) { - return + if r.questionnaireResolver.Assessed(a.Assessments) { + assessedCount++ } } - assessed = true + assessed = assessedCount > 0 && assessedCount == len(archetypes) return } diff --git a/assessment/archetype.go b/assessment/archetype.go index 550330d2..ffe3cf64 100644 --- a/assessment/archetype.go +++ b/assessment/archetype.go @@ -2,13 +2,35 @@ package assessment import "github.com/konveyor/tackle2-hub/model" +// +// Archetype represents an Archetype with its assessments. +type Archetype struct { + *model.Archetype + Assessments []Assessment +} + +// +// With updates the Archetype with the db model and deserializes its assessments. +func (r *Archetype) With(m *model.Archetype) { + r.Archetype = m + for _, a := range m.Assessments { + assessment := Assessment{} + assessment.With(&a) + r.Assessments = append(r.Assessments, assessment) + } +} + // // NewArchetypeResolver creates a new ArchetypeResolver. -func NewArchetypeResolver(archetype *model.Archetype, tags *TagResolver) (a *ArchetypeResolver) { +func NewArchetypeResolver(m *model.Archetype, tags *TagResolver, membership *MembershipResolver, questionnaire *QuestionnaireResolver) (a *ArchetypeResolver) { a = &ArchetypeResolver{ - archetype: archetype, - tagResolver: tags, + tags: tags, + membership: membership, + questionnaire: questionnaire, } + archetype := Archetype{} + archetype.With(m) + a.archetype = archetype return } @@ -16,17 +38,22 @@ func NewArchetypeResolver(archetype *model.Archetype, tags *TagResolver) (a *Arc // ArchetypeResolver wraps an Archetype model // with assessment-related functionality. type ArchetypeResolver struct { - archetype *model.Archetype - tagResolver *TagResolver + archetype Archetype + tags *TagResolver + membership *MembershipResolver + questionnaire *QuestionnaireResolver } // // AssessmentTags returns the list of tags that the archetype should // inherit from the answers given to its assessments. func (r *ArchetypeResolver) AssessmentTags() (tags []model.Tag) { + if r.tags == nil { + return + } seenTags := make(map[uint]bool) for _, assessment := range r.archetype.Assessments { - aTags := r.tagResolver.Assessment(&assessment) + aTags := r.tags.Assessment(assessment) for _, t := range aTags { if _, found := seenTags[t.ID]; !found { seenTags[t.ID] = true @@ -36,3 +63,37 @@ func (r *ArchetypeResolver) AssessmentTags() (tags []model.Tag) { } return } + +// +// Risk returns the overall risk level for the archetypes' assessments. +func (r *ArchetypeResolver) Risk() (risk string) { + risk = Risk(r.archetype.Assessments) + return +} + +// +// Confidence returns the archetype's overall assessment confidence score. +func (r *ArchetypeResolver) Confidence() (confidence int) { + confidence = Confidence(r.archetype.Assessments) + return +} + +// +// Assessed returns whether the archetype has been fully assessed. +func (r *ArchetypeResolver) Assessed() (assessed bool) { + if r.questionnaire == nil { + return + } + assessed = r.questionnaire.Assessed(r.archetype.Assessments) + return +} + +// +// Applications returns the archetype's member applications. +func (r *ArchetypeResolver) Applications() (applications []Application, err error) { + if r.membership == nil { + return + } + applications, err = r.membership.Applications(r.archetype) + return +} diff --git a/assessment/section.go b/assessment/assessment.go similarity index 56% rename from assessment/section.go rename to assessment/assessment.go index 9cbea46b..c02b7335 100644 --- a/assessment/section.go +++ b/assessment/assessment.go @@ -1,5 +1,129 @@ package assessment +import ( + "encoding/json" + "github.com/konveyor/tackle2-hub/model" + "math" +) + +// +// Assessment represents a deserialized Assessment. +type Assessment struct { + *model.Assessment + Sections []Section `json:"sections"` + Thresholds Thresholds `json:"thresholds"` + RiskMessages RiskMessages `json:"riskMessages"` +} + +// +// With updates the Assessment with the db model and deserializes its fields. +func (r *Assessment) With(m *model.Assessment) { + r.Assessment = m + _ = json.Unmarshal(m.Sections, &r.Sections) + _ = json.Unmarshal(m.Thresholds, &r.Thresholds) + _ = json.Unmarshal(m.RiskMessages, &r.RiskMessages) +} + +// +// Status returns the started status of the assessment. +func (r *Assessment) Status() string { + if r.Complete() { + return StatusComplete + } else if r.Started() { + return StatusStarted + } else { + return StatusEmpty + } +} + +// +// Complete returns whether all sections have been completed. +func (r *Assessment) Complete() bool { + for _, s := range r.Sections { + if !s.Complete() { + return false + } + } + return true +} + +// +// Started returns whether any sections have been started. +func (r *Assessment) Started() bool { + for _, s := range r.Sections { + if s.Started() { + return true + } + } + return false +} + +// +// Risk calculates the risk level (red, yellow, green, unknown) for the application. +func (r *Assessment) Risk() string { + var total uint + colors := make(map[string]uint) + for _, s := range r.Sections { + for _, risk := range s.Risks() { + colors[risk]++ + total++ + } + } + if total == 0 { + return RiskUnknown + } + if (float64(colors[RiskRed]) / float64(total)) >= (float64(r.Thresholds.Red) / float64(100)) { + return RiskRed + } + if (float64(colors[RiskYellow]) / float64(total)) >= (float64(r.Thresholds.Yellow) / float64(100)) { + return RiskYellow + } + if (float64(colors[RiskUnknown]) / float64(total)) >= (float64(r.Thresholds.Unknown) / float64(100)) { + return RiskUnknown + } + return RiskGreen +} + +// +// Confidence calculates a confidence score based on the answers to an assessment's questions. +// The algorithm is a reimplementation of the calculation done by Pathfinder. +func (r *Assessment) Confidence() (score int) { + totalQuestions := 0 + riskCounts := make(map[string]int) + for _, s := range r.Sections { + for _, r := range s.Risks() { + riskCounts[r]++ + totalQuestions++ + } + } + if totalQuestions == 0 { + return + } + adjuster := 1.0 + if riskCounts[RiskRed] > 0 { + adjuster = adjuster * math.Pow(AdjusterRed, float64(riskCounts[RiskRed])) + } + if riskCounts[RiskYellow] > 0 { + adjuster = adjuster * math.Pow(AdjusterYellow, float64(riskCounts[RiskYellow])) + } + confidence := 0.0 + for i := 0; i < riskCounts[RiskRed]; i++ { + confidence *= MultiplierRed + confidence += WeightRed * adjuster + } + for i := 0; i < riskCounts[RiskYellow]; i++ { + confidence *= MultiplierYellow + confidence += WeightYellow * adjuster + } + confidence += float64(riskCounts[RiskGreen]) * WeightGreen * adjuster + confidence += float64(riskCounts[RiskUnknown]) * WeightUnknown * adjuster + + maxConfidence := WeightGreen * totalQuestions + score = int(confidence / float64(maxConfidence) * 100) + + return +} + // // Section represents a group of questions in a questionnaire. type Section struct { diff --git a/assessment/membership.go b/assessment/membership.go index 2119e001..4a7e2b25 100644 --- a/assessment/membership.go +++ b/assessment/membership.go @@ -12,7 +12,7 @@ import ( func NewMembershipResolver(db *gorm.DB) (m *MembershipResolver) { m = &MembershipResolver{db: db} m.tagSets = make(map[uint]Set) - m.archetypeMembers = make(map[uint][]model.Application) + m.archetypeMembers = make(map[uint][]Application) return } @@ -20,15 +20,15 @@ func NewMembershipResolver(db *gorm.DB) (m *MembershipResolver) { // MembershipResolver resolves archetype membership. type MembershipResolver struct { db *gorm.DB - archetypes []model.Archetype + archetypes []Archetype tagSets map[uint]Set - archetypeMembers map[uint][]model.Application + archetypeMembers map[uint][]Application membersCached bool } // // Applications returns the list of applications that are members of the given archetype. -func (r *MembershipResolver) Applications(m *model.Archetype) (applications []model.Application, err error) { +func (r *MembershipResolver) Applications(m Archetype) (applications []Application, err error) { err = r.cacheArchetypeMembers() if err != nil { return @@ -41,18 +41,18 @@ func (r *MembershipResolver) Applications(m *model.Archetype) (applications []mo // // Archetypes returns the list of archetypes that the application is a member of. -func (r *MembershipResolver) Archetypes(m *model.Application) (archetypes []model.Archetype, err error) { +func (r *MembershipResolver) Archetypes(app Application) (archetypes []Archetype, err error) { err = r.cacheArchetypes() if err != nil { return } appTags := NewSet() - for _, t := range m.Tags { + for _, t := range app.Tags { appTags.Add(t.ID) } - matches := []model.Archetype{} + matches := []Archetype{} for _, a := range r.archetypes { if appTags.Superset(r.tagSets[a.ID], false) { matches = append(matches, a) @@ -74,7 +74,7 @@ loop: } } archetypes = append(archetypes, a1) - r.archetypeMembers[a1.ID] = append(r.archetypeMembers[a1.ID], *m) + r.archetypeMembers[a1.ID] = append(r.archetypeMembers[a1.ID], app) } return @@ -85,14 +85,18 @@ func (r *MembershipResolver) cacheArchetypes() (err error) { return } + list := []model.Archetype{} db := r.db.Preload(clause.Associations) - result := db.Find(&r.archetypes) + result := db.Find(&list) if result.Error != nil { err = liberr.Wrap(err) return } - for _, a := range r.archetypes { + for i := range list { + a := Archetype{} + a.With(&list[i]) + r.archetypes = append(r.archetypes, a) set := NewSet() for _, t := range a.CriteriaTags { set.Add(t.ID) @@ -107,14 +111,17 @@ func (r *MembershipResolver) cacheArchetypeMembers() (err error) { if r.membersCached { return } - allApplications := []model.Application{} - result := r.db.Preload("Tags").Find(&allApplications) + list := []model.Application{} + result := r.db.Preload("Tags").Find(&list) if result.Error != nil { err = liberr.Wrap(err) return } - for _, a := range allApplications { - _, aErr := r.Archetypes(&a) + + for i := range list { + a := Application{} + a.With(&list[i]) + _, aErr := r.Archetypes(a) if aErr != nil { err = aErr return diff --git a/assessment/pkg.go b/assessment/pkg.go index 6476314b..0f995178 100644 --- a/assessment/pkg.go +++ b/assessment/pkg.go @@ -3,7 +3,6 @@ package assessment import ( "encoding/json" "github.com/konveyor/tackle2-hub/model" - "math" ) // @@ -15,6 +14,14 @@ const ( RiskGreen = "green" ) +// +// Assessment status +const ( + StatusEmpty = "empty" + StatusStarted = "started" + StatusComplete = "complete" +) + // // Confidence adjustment const ( @@ -39,39 +46,55 @@ const ( ) // -// Confidence calculates a confidence score based on the answers to an assessment's questions. -// The algorithm is a reimplementation of the calculation done by Pathfinder. -func Confidence(sections []Section) (score int) { - totalQuestions := 0 - riskCounts := make(map[string]int) - for _, s := range sections { - for _, r := range s.Risks() { - riskCounts[r]++ - totalQuestions++ - } - } - adjuster := 1.0 - if riskCounts[RiskRed] > 0 { - adjuster = adjuster * math.Pow(AdjusterRed, float64(riskCounts[RiskRed])) +// Risk returns the single highest risk score for a group of assessments. +func Risk(assessments []Assessment) (risk string) { + risk = RiskUnknown + if len(assessments) == 0 { + return } - if riskCounts[RiskYellow] > 0 { - adjuster = adjuster * math.Pow(AdjusterYellow, float64(riskCounts[RiskYellow])) - } - confidence := 0.0 - for i := 0; i < riskCounts[RiskRed]; i++ { - confidence *= MultiplierRed - confidence += WeightRed * adjuster + red := 0 + yellow := 0 + unknown := 0 + green := 0 + if len(assessments) > 0 { + for _, a := range assessments { + switch a.Risk() { + case RiskRed: + red++ + case RiskYellow: + yellow++ + case RiskGreen: + green++ + default: + unknown++ + } + } } - for i := 0; i < riskCounts[RiskYellow]; i++ { - confidence *= MultiplierYellow - confidence += WeightYellow * adjuster + + switch { + case red > 0: + risk = RiskRed + case unknown > 0: + risk = RiskUnknown + case yellow > 0: + risk = RiskYellow + case green == len(assessments): + risk = RiskGreen } - confidence += float64(riskCounts[RiskGreen]) * WeightGreen * adjuster - confidence += float64(riskCounts[RiskUnknown]) * WeightUnknown * adjuster - maxConfidence := WeightGreen * totalQuestions - score = int(confidence / float64(maxConfidence) * 100) + return +} +// +// Confidence returns a total confidence score for a group of assessments. +func Confidence(assessments []Assessment) (confidence int) { + if len(assessments) == 0 { + return + } + for _, a := range assessments { + confidence += a.Confidence() + } + confidence /= len(assessments) return } diff --git a/assessment/questionnaire.go b/assessment/questionnaire.go index c1e1a3a4..5d119206 100644 --- a/assessment/questionnaire.go +++ b/assessment/questionnaire.go @@ -1,7 +1,6 @@ package assessment import ( - "encoding/json" liberr "github.com/jortel/go-utils/error" "github.com/konveyor/tackle2-hub/model" "gorm.io/gorm" @@ -45,14 +44,12 @@ func (r *QuestionnaireResolver) cacheQuestionnaires() (err error) { // // Assessed returns whether a slice contains a completed assessment for each of the required // questionnaires. -func (r *QuestionnaireResolver) Assessed(assessments []model.Assessment) (assessed bool) { +func (r *QuestionnaireResolver) Assessed(assessments []Assessment) (assessed bool) { answered := NewSet() loop: for _, a := range assessments { if r.requiredQuestionnaires.Contains(a.QuestionnaireID) { - sections := []Section{} - _ = json.Unmarshal(a.Sections, §ions) - for _, s := range sections { + for _, s := range a.Sections { if !s.Complete() { continue loop } diff --git a/assessment/tag.go b/assessment/tag.go index 25c8ee55..073d0046 100644 --- a/assessment/tag.go +++ b/assessment/tag.go @@ -1,7 +1,6 @@ package assessment import ( - "encoding/json" liberr "github.com/jortel/go-utils/error" "github.com/konveyor/tackle2-hub/model" "gorm.io/gorm" @@ -34,10 +33,8 @@ func (r *TagResolver) Resolve(category string, tag string) (t *model.Tag, found // // Assessment returns all the Tag models that should be applied from the assessment. -func (r *TagResolver) Assessment(assessment *model.Assessment) (tags []model.Tag) { - sections := []Section{} - _ = json.Unmarshal(assessment.Sections, §ions) - for _, s := range sections { +func (r *TagResolver) Assessment(assessment Assessment) (tags []model.Tag) { + for _, s := range assessment.Sections { for _, t := range s.Tags() { tag, found := r.Resolve(t.Category, t.Tag) if found {