mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
Cross-tabulates the properties that fired against the human verdict, one cell per implementation, over a sweep whose implementations all passed their own generated tests. An implementation that failed to build, has no usable run, or carries no filed verdict is listed as missing data rather than counted as a clean cell. Landing the package in one commit because the intermediate splits would not link.
511 lines
17 KiB
Go
511 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The four cells of the pre-registered measure, named as
|
|
// model-implementations.md describes them.
|
|
const (
|
|
cellTruePositive = "checker fired, review confirmed"
|
|
cellFalsePositive = "checker fired, review found nothing"
|
|
cellFalseNegative = "checker silent, review found a defect"
|
|
cellTrueNegative = "checker silent, review found nothing"
|
|
)
|
|
|
|
// Reasons an implementation is missing data rather than a cell of the matrix.
|
|
const (
|
|
missingSweepStage = "sweep stopped before any run"
|
|
missingNoUsableRun = "no usable run"
|
|
missingNoSweepRecord = "no sweep record"
|
|
missingNoReview = "no verdict filed"
|
|
missingMalformed = "malformed verdict form"
|
|
missingSurfaces = "surface locatability unknown"
|
|
missingNoModel = "not in the assignment mapping"
|
|
)
|
|
|
|
type matrix struct {
|
|
Unit string `json:"unit"`
|
|
Scored int `json:"scored"`
|
|
TruePositive int `json:"true_positive"`
|
|
FalsePositive int `json:"false_positive"`
|
|
FalseNegative int `json:"false_negative"`
|
|
TrueNegative int `json:"true_negative"`
|
|
Precision *float64 `json:"precision"`
|
|
Recall *float64 `json:"recall"`
|
|
}
|
|
|
|
func (m *matrix) add(checkerPositive, humanPositive bool) {
|
|
m.Scored++
|
|
switch {
|
|
case checkerPositive && humanPositive:
|
|
m.TruePositive++
|
|
case checkerPositive:
|
|
m.FalsePositive++
|
|
case humanPositive:
|
|
m.FalseNegative++
|
|
default:
|
|
m.TrueNegative++
|
|
}
|
|
}
|
|
|
|
func (m *matrix) finish() {
|
|
m.Precision = ratio(m.TruePositive, m.TruePositive+m.FalsePositive)
|
|
m.Recall = ratio(m.TruePositive, m.TruePositive+m.FalseNegative)
|
|
}
|
|
|
|
func ratio(numerator, denominator int) *float64 {
|
|
if denominator == 0 {
|
|
return nil
|
|
}
|
|
value := float64(numerator) / float64(denominator)
|
|
return &value
|
|
}
|
|
|
|
// clauseMatrix scores one implementation-clause pair. Its three side buckets
|
|
// hold the pairs that are not evidence either way: a clause the reviewer could
|
|
// not judge, a clause whose every covering property was unevaluable because a
|
|
// surface was never located, and, tracked but still scored, a clause no
|
|
// property covers at all.
|
|
type clauseMatrix struct {
|
|
matrix
|
|
CannotTell int `json:"cannot_tell"`
|
|
UnevaluatedSurfaceMissed int `json:"unevaluated_surface_missed"`
|
|
UncoveredScored int `json:"uncovered_clause_pairs_scored"`
|
|
UncoveredFalseNegative int `json:"uncovered_clause_false_negatives"`
|
|
}
|
|
|
|
type implementationOutcome struct {
|
|
Implementation string `json:"implementation"`
|
|
Model string `json:"model"`
|
|
Cell string `json:"cell"`
|
|
FiredProperties []string `json:"fired_properties,omitempty"`
|
|
ViolatedClauses []string `json:"violated_clauses,omitempty"`
|
|
CannotTellClauses []string `json:"cannot_tell_clauses,omitempty"`
|
|
UnlocatableSurfaces []string `json:"unlocatable_surfaces,omitempty"`
|
|
UnevaluatedProperties []string `json:"unevaluated_properties,omitempty"`
|
|
// DefectOnlyOnUnevaluatedClauses marks a false negative the checker was
|
|
// never in a position to catch: every clause the reviewer faulted is
|
|
// covered only by properties a missing surface left unevaluable. It is a
|
|
// portability miss reported beside the matrix, never inside it.
|
|
DefectOnlyOnUnevaluatedClauses bool `json:"defect_only_on_unevaluated_clauses,omitempty"`
|
|
RunsUsable int `json:"runs_usable"`
|
|
ReviewMinutes int `json:"review_minutes,omitempty"`
|
|
}
|
|
|
|
type exclusion struct {
|
|
Implementation string `json:"implementation"`
|
|
Model string `json:"model,omitempty"`
|
|
Reason string `json:"reason"`
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
type modelBreakdown struct {
|
|
Model string `json:"model"`
|
|
Implementations matrix `json:"implementation_matrix"`
|
|
Clauses clauseMatrix `json:"clause_matrix"`
|
|
Excluded int `json:"excluded"`
|
|
DefectOnlyOnUnevaluatedClauses int `json:"defect_only_on_unevaluated_clauses"`
|
|
}
|
|
|
|
type portability struct {
|
|
Scored int `json:"implementations_scored"`
|
|
WithUnlocatableSurface int `json:"implementations_with_an_unlocatable_surface"`
|
|
BySurface map[string]int `json:"implementations_by_unlocatable_surface,omitempty"`
|
|
SurfacesReadAsInconclusive []string `json:"surfaces_a_miss_cannot_be_read_from,omitempty"`
|
|
}
|
|
|
|
type coverage struct {
|
|
MappedProperties int `json:"mapped_properties"`
|
|
MappingTodoRows int `json:"mapping_todo_rows"`
|
|
ClausesCovered []string `json:"clauses_covered,omitempty"`
|
|
ClausesUncovered []string `json:"clauses_no_property_covers,omitempty"`
|
|
FiredPropertiesNotMapped []string `json:"fired_properties_not_in_the_mapping,omitempty"`
|
|
}
|
|
|
|
type result struct {
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
SweepDirectory string `json:"sweep_directory"`
|
|
ReviewsDirectory string `json:"reviews_directory"`
|
|
MappingPath string `json:"property_clause_mapping"`
|
|
SpecPath string `json:"spec_path,omitempty"`
|
|
Implementations matrix `json:"implementation_matrix"`
|
|
Clauses clauseMatrix `json:"clause_matrix"`
|
|
ByModel []modelBreakdown `json:"by_model"`
|
|
Outcomes []implementationOutcome `json:"outcomes"`
|
|
Excluded []exclusion `json:"excluded,omitempty"`
|
|
Portability portability `json:"portability"`
|
|
ReviewMinutes int `json:"review_minutes_over_scored_implementations"`
|
|
Coverage coverage `json:"clause_coverage"`
|
|
Notes []string `json:"notes,omitempty"`
|
|
}
|
|
|
|
func crossTabulate(
|
|
checker checkerSide,
|
|
reviews reviewSide,
|
|
assignments map[string]string,
|
|
declared mapping,
|
|
now time.Time,
|
|
) result {
|
|
outcome := result{
|
|
GeneratedAt: now,
|
|
SweepDirectory: checker.Directory,
|
|
ReviewsDirectory: reviews.Directory,
|
|
MappingPath: declared.Path,
|
|
SpecPath: checker.SpecPath,
|
|
Implementations: matrix{Unit: "implementation"},
|
|
Clauses: clauseMatrix{matrix: matrix{Unit: "implementation-clause pair"}},
|
|
Portability: portability{BySurface: map[string]int{}},
|
|
}
|
|
|
|
reviewByName := map[string]reviewVerdict{}
|
|
for _, verdict := range reviews.Verdicts {
|
|
reviewByName[verdict.Implementation] = verdict
|
|
}
|
|
malformedByName := map[string]malformedReview{}
|
|
for _, entry := range reviews.Malformed {
|
|
malformedByName[entry.Implementation] = entry
|
|
}
|
|
checkerByName := map[string]checkerVerdict{}
|
|
for _, verdict := range checker.Verdicts {
|
|
checkerByName[verdict.Implementation] = verdict
|
|
}
|
|
|
|
byModel := map[string]*modelBreakdown{}
|
|
modelOf := func(name string) string { return assignments[name] }
|
|
breakdown := func(model string) *modelBreakdown {
|
|
current, seen := byModel[model]
|
|
if !seen {
|
|
current = &modelBreakdown{
|
|
Model: model,
|
|
Implementations: matrix{Unit: "implementation"},
|
|
Clauses: clauseMatrix{matrix: matrix{Unit: "implementation-clause pair"}},
|
|
}
|
|
byModel[model] = current
|
|
}
|
|
return current
|
|
}
|
|
|
|
unmappedFired := map[string]bool{}
|
|
for _, name := range implementationNames(checker, reviews, assignments) {
|
|
model := modelOf(name)
|
|
verdict, swept := checkerByName[name]
|
|
review, reviewed := reviewByName[name]
|
|
|
|
if reason, detail := missingData(name, model, verdict, swept, reviewed, malformedByName); reason != "" {
|
|
outcome.Excluded = append(outcome.Excluded, exclusion{
|
|
Implementation: name, Model: model, Reason: reason, Detail: detail,
|
|
})
|
|
if model != "" {
|
|
breakdown(model).Excluded++
|
|
}
|
|
continue
|
|
}
|
|
|
|
for _, property := range verdict.FiredProperties {
|
|
if !declared.knows(property) {
|
|
unmappedFired[property] = true
|
|
}
|
|
}
|
|
row := scoreImplementation(verdict, review, declared)
|
|
row.Model = model
|
|
outcome.Outcomes = append(outcome.Outcomes, row)
|
|
|
|
modelRow := breakdown(model)
|
|
outcome.Implementations.add(verdict.fired(), review.defective())
|
|
modelRow.Implementations.add(verdict.fired(), review.defective())
|
|
scoreClauses(&outcome.Clauses, &modelRow.Clauses, verdict, review, declared)
|
|
|
|
if row.DefectOnlyOnUnevaluatedClauses {
|
|
modelRow.DefectOnlyOnUnevaluatedClauses++
|
|
}
|
|
outcome.Portability.Scored++
|
|
outcome.ReviewMinutes += review.Minutes
|
|
if len(row.UnlocatableSurfaces) > 0 {
|
|
outcome.Portability.WithUnlocatableSurface++
|
|
for _, surface := range row.UnlocatableSurfaces {
|
|
outcome.Portability.BySurface[surface]++
|
|
}
|
|
}
|
|
}
|
|
|
|
outcome.Implementations.finish()
|
|
outcome.Clauses.finish()
|
|
for _, model := range capabilityOrder {
|
|
current, seen := byModel[model]
|
|
if !seen {
|
|
continue
|
|
}
|
|
current.Implementations.finish()
|
|
current.Clauses.finish()
|
|
outcome.ByModel = append(outcome.ByModel, *current)
|
|
}
|
|
|
|
outcome.Coverage = describeCoverage(declared, unmappedFired)
|
|
outcome.Portability.SurfacesReadAsInconclusive = inconclusiveSurfaces(declared)
|
|
outcome.Notes = buildNotes(outcome, declared, reviews)
|
|
return outcome
|
|
}
|
|
|
|
func implementationNames(checker checkerSide, reviews reviewSide, assignments map[string]string) []string {
|
|
names := map[string]bool{}
|
|
for _, planned := range checker.Planned {
|
|
names[planned] = true
|
|
}
|
|
for _, verdict := range checker.Verdicts {
|
|
names[verdict.Implementation] = true
|
|
}
|
|
for _, verdict := range reviews.Verdicts {
|
|
names[verdict.Implementation] = true
|
|
}
|
|
for _, entry := range reviews.Malformed {
|
|
names[entry.Implementation] = true
|
|
}
|
|
for name := range assignments {
|
|
names[name] = true
|
|
}
|
|
return slices.Sorted(maps.Keys(names))
|
|
}
|
|
|
|
// missingData names why an implementation carries no cell. A build that never
|
|
// finished, a run that never produced a usable campaign and a verdict that was
|
|
// never filed are all absent evidence: scoring any of them as a clean run would
|
|
// read the gap as agreement.
|
|
func missingData(
|
|
name string,
|
|
model string,
|
|
verdict checkerVerdict,
|
|
swept bool,
|
|
reviewed bool,
|
|
malformed map[string]malformedReview,
|
|
) (string, string) {
|
|
if model == "" {
|
|
return missingNoModel, ""
|
|
}
|
|
if !swept {
|
|
return missingNoSweepRecord, ""
|
|
}
|
|
if verdict.FailedStage != "" {
|
|
return missingSweepStage, fmt.Sprintf("%s: %s", verdict.FailedStage, verdict.FailedError)
|
|
}
|
|
if verdict.RunsUsable == 0 {
|
|
return missingNoUsableRun, excludedSummary(verdict)
|
|
}
|
|
if entry, broken := malformed[name]; broken {
|
|
return missingMalformed, entry.Reason
|
|
}
|
|
if !reviewed {
|
|
return missingNoReview, ""
|
|
}
|
|
if !verdict.SurfacesKnown {
|
|
return missingSurfaces, strings.Join(verdict.TraceErrors, "; ")
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
func excludedSummary(verdict checkerVerdict) string {
|
|
if len(verdict.ExcludedByReason) == 0 {
|
|
return ""
|
|
}
|
|
var parts []string
|
|
for _, reason := range slices.Sorted(maps.Keys(verdict.ExcludedByReason)) {
|
|
parts = append(parts, fmt.Sprintf("%s=%d", reason, verdict.ExcludedByReason[reason]))
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
func scoreImplementation(verdict checkerVerdict, review reviewVerdict, declared mapping) implementationOutcome {
|
|
row := implementationOutcome{
|
|
Implementation: verdict.Implementation,
|
|
Cell: cellOf(verdict.fired(), review.defective()),
|
|
FiredProperties: verdict.FiredProperties,
|
|
ViolatedClauses: review.violatedClauses(),
|
|
RunsUsable: verdict.RunsUsable,
|
|
ReviewMinutes: review.Minutes,
|
|
}
|
|
for _, clause := range allClauses() {
|
|
if review.Clauses[clause] == clauseCannotTell {
|
|
row.CannotTellClauses = append(row.CannotTellClauses, clause)
|
|
}
|
|
}
|
|
for _, surface := range declared.unlocatableSurfaces() {
|
|
if !verdict.SurfacesObserved[surface] {
|
|
row.UnlocatableSurfaces = append(row.UnlocatableSurfaces, surface)
|
|
}
|
|
}
|
|
for _, property := range declared.Properties {
|
|
if declared.unevaluable(property.Property, verdict.SurfacesObserved) {
|
|
row.UnevaluatedProperties = append(row.UnevaluatedProperties, property.Property)
|
|
}
|
|
}
|
|
row.DefectOnlyOnUnevaluatedClauses = attributableToUnevaluated(verdict, review, declared)
|
|
return row
|
|
}
|
|
|
|
// attributableToUnevaluated reports a false negative the checker could not have
|
|
// caught: it fired nothing, the reviewer faulted at least one clause, and every
|
|
// clause the reviewer faulted is covered only by properties a missing surface
|
|
// left unevaluable.
|
|
func attributableToUnevaluated(verdict checkerVerdict, review reviewVerdict, declared mapping) bool {
|
|
if verdict.fired() || !review.defective() {
|
|
return false
|
|
}
|
|
violated := review.violatedClauses()
|
|
if len(violated) == 0 {
|
|
return false
|
|
}
|
|
for _, clause := range violated {
|
|
covering := declared.covering(clause)
|
|
if len(covering) == 0 {
|
|
return false
|
|
}
|
|
for _, property := range covering {
|
|
if !declared.unevaluable(property, verdict.SurfacesObserved) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func cellOf(checkerPositive, humanPositive bool) string {
|
|
switch {
|
|
case checkerPositive && humanPositive:
|
|
return cellTruePositive
|
|
case checkerPositive:
|
|
return cellFalsePositive
|
|
case humanPositive:
|
|
return cellFalseNegative
|
|
default:
|
|
return cellTrueNegative
|
|
}
|
|
}
|
|
|
|
func scoreClauses(overall, model *clauseMatrix, verdict checkerVerdict, review reviewVerdict, declared mapping) {
|
|
fired := map[string]bool{}
|
|
for _, property := range verdict.FiredProperties {
|
|
fired[property] = true
|
|
}
|
|
for _, clause := range allClauses() {
|
|
label := review.Clauses[clause]
|
|
if label == clauseCannotTell {
|
|
overall.CannotTell++
|
|
model.CannotTell++
|
|
continue
|
|
}
|
|
covering := declared.covering(clause)
|
|
checkerPositive := false
|
|
for _, property := range covering {
|
|
if fired[property] {
|
|
checkerPositive = true
|
|
break
|
|
}
|
|
}
|
|
if !checkerPositive && len(covering) > 0 && allUnevaluable(covering, verdict, declared) {
|
|
overall.UnevaluatedSurfaceMissed++
|
|
model.UnevaluatedSurfaceMissed++
|
|
continue
|
|
}
|
|
humanPositive := label == clauseViolates
|
|
overall.add(checkerPositive, humanPositive)
|
|
model.add(checkerPositive, humanPositive)
|
|
if len(covering) == 0 {
|
|
overall.UncoveredScored++
|
|
model.UncoveredScored++
|
|
if humanPositive {
|
|
overall.UncoveredFalseNegative++
|
|
model.UncoveredFalseNegative++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func allUnevaluable(covering []string, verdict checkerVerdict, declared mapping) bool {
|
|
for _, property := range covering {
|
|
if !declared.unevaluable(property, verdict.SurfacesObserved) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func describeCoverage(declared mapping, unmappedFired map[string]bool) coverage {
|
|
result := coverage{
|
|
MappedProperties: len(declared.Properties),
|
|
MappingTodoRows: declared.PropertyTodoRows,
|
|
}
|
|
for _, clause := range allClauses() {
|
|
if len(declared.covering(clause)) > 0 {
|
|
result.ClausesCovered = append(result.ClausesCovered, clause)
|
|
continue
|
|
}
|
|
result.ClausesUncovered = append(result.ClausesUncovered, clause)
|
|
}
|
|
if len(unmappedFired) > 0 {
|
|
result.FiredPropertiesNotMapped = slices.Sorted(maps.Keys(unmappedFired))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func inconclusiveSurfaces(declared mapping) []string {
|
|
var names []string
|
|
for _, surface := range declared.Surfaces {
|
|
if surface.NeverObserved == surfaceInconclusive {
|
|
names = append(names, surface.Surface)
|
|
}
|
|
}
|
|
return names
|
|
}
|
|
|
|
func buildNotes(outcome result, declared mapping, reviews reviewSide) []string {
|
|
var notes []string
|
|
if len(declared.Properties) == 0 {
|
|
notes = append(notes, fmt.Sprintf(
|
|
"%s maps no property to a clause, so the clause matrix is empty and no portability miss can be detected; "+
|
|
"the implementation matrix below stands on its own", declared.Path))
|
|
}
|
|
if declared.PropertyTodoRows > 0 {
|
|
notes = append(notes, fmt.Sprintf("%s still carries %d TODO row(s) in its property table",
|
|
declared.Path, declared.PropertyTodoRows))
|
|
}
|
|
if len(outcome.Coverage.FiredPropertiesNotMapped) > 0 {
|
|
notes = append(notes, fmt.Sprintf(
|
|
"%d fired propert(ies) are absent from the mapping and could not be attributed to a clause: %s",
|
|
len(outcome.Coverage.FiredPropertiesNotMapped),
|
|
strings.Join(outcome.Coverage.FiredPropertiesNotMapped, ", ")))
|
|
}
|
|
for _, verdict := range reviews.Verdicts {
|
|
if !verdict.defective() && len(verdict.violatedClauses()) > 0 {
|
|
notes = append(notes, fmt.Sprintf(
|
|
"%s files %d violating clause(s) under an overall verdict of %s; the overall verdict is what the matrix scores",
|
|
verdict.Implementation, len(verdict.violatedClauses()), overallNotDefective))
|
|
}
|
|
if len(verdict.Adjudicated) > 0 {
|
|
notes = append(notes, fmt.Sprintf("%s uses the adjudicated label for %s",
|
|
verdict.Implementation, strings.Join(verdict.Adjudicated, ", ")))
|
|
}
|
|
}
|
|
if count := attributedFalseNegatives(outcome); count > 0 {
|
|
notes = append(notes, fmt.Sprintf(
|
|
"%d false negative(s) fault only clauses whose every property a missing surface left unevaluable: "+
|
|
"those are portability misses, not blind spots", count))
|
|
}
|
|
notes = append(notes, "second-rater agreement and Cohen's kappa are not computed here; "+
|
|
"an impl-NN-adjudication.md is read and its resolved labels replace the first rater's")
|
|
return notes
|
|
}
|
|
|
|
func attributedFalseNegatives(outcome result) int {
|
|
count := 0
|
|
for _, row := range outcome.Outcomes {
|
|
if row.DefectOnlyOnUnevaluatedClauses {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|