diff --git a/cmd/internal-tools/confusion-matrix/assignment.go b/cmd/internal-tools/confusion-matrix/assignment.go
new file mode 100644
index 0000000..e0d6924
--- /dev/null
+++ b/cmd/internal-tools/confusion-matrix/assignment.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "regexp"
+ "strings"
+)
+
+// The three models the sample is drawn from, in the capability order
+// model-implementations.md fixed before any implementation was generated.
+var capabilityOrder = []string{"Sonnet 5", "Opus 5", "Fable 5"}
+
+var (
+ implementationName = regexp.MustCompile(`^impl-\d+$`)
+ nonAlphanumeric = regexp.MustCompile(`[^a-z0-9]`)
+)
+
+func loadAssignment(path string) (map[string]string, error) {
+ body, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("read assignment: %w", err)
+ }
+ assignments := map[string]string{}
+ for _, row := range parseTableRows(string(body)) {
+ name := row.cell(0)
+ if !implementationName.MatchString(name) {
+ continue
+ }
+ model, ok := canonicalModel(row.cell(1))
+ if !ok {
+ return nil, fmt.Errorf("%s line %d: %s is assigned model %q, which is none of %s",
+ path, row.Line, name, row.cell(1), strings.Join(capabilityOrder, ", "))
+ }
+ if existing, seen := assignments[name]; seen && existing != model {
+ return nil, fmt.Errorf("%s line %d: %s is assigned to both %s and %s",
+ path, row.Line, name, existing, model)
+ }
+ assignments[name] = model
+ }
+ if len(assignments) == 0 {
+ return nil, fmt.Errorf("%s maps no implementation to a model", path)
+ }
+ return assignments, nil
+}
+
+func canonicalModel(value string) (string, bool) {
+ key := nonAlphanumeric.ReplaceAllString(strings.ToLower(value), "")
+ for _, model := range capabilityOrder {
+ if key == nonAlphanumeric.ReplaceAllString(strings.ToLower(model), "") {
+ return model, true
+ }
+ }
+ return "", false
+}
diff --git a/cmd/internal-tools/confusion-matrix/checker.go b/cmd/internal-tools/confusion-matrix/checker.go
new file mode 100644
index 0000000..c0ffabf
--- /dev/null
+++ b/cmd/internal-tools/confusion-matrix/checker.go
@@ -0,0 +1,329 @@
+package main
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "maps"
+ "os"
+ "path/filepath"
+ "slices"
+ "strconv"
+ "strings"
+)
+
+const (
+ sweepManifestFileName = "sweep.json"
+ sweepRecordsFileName = "implementations.jsonl"
+ campaignManifestFileName = "campaign.json"
+ campaignRecordsFileName = "runs.jsonl"
+ traceFileName = "trace.jsonl"
+ surfacesExtractor = "locatableSurfaces"
+ maxRecordBytes = 4 * 1024 * 1024
+ maxTraceLineBytes = 16 * 1024 * 1024
+)
+
+// Run exclusion reasons, kept in the vocabulary analyze already uses so the two
+// tools describe the same run the same way.
+const (
+ reasonLaunchError = "launch error"
+ reasonTimedOut = "timed out"
+ reasonNonzeroExit = "nonzero exit"
+ reasonTraceError = "unreadable trace"
+)
+
+type sweepManifest struct {
+ SpecPath string `json:"spec_path"`
+ Implementations []struct {
+ Name string `json:"name"`
+ } `json:"implementations"`
+}
+
+type sweepRunRecord struct {
+ Seed int64 `json:"seed"`
+ ExitCode int `json:"exit_code"`
+ LaunchError string `json:"launch_error"`
+ CampaignDirectory string `json:"campaign_directory"`
+}
+
+type sweepImplementationRecord struct {
+ Name string `json:"implementation"`
+ FailedStage string `json:"failed_stage"`
+ Error string `json:"error"`
+ Runs []sweepRunRecord `json:"runs"`
+}
+
+type campaignRunRecord struct {
+ Seed int64 `json:"seed"`
+ ExitCode int `json:"exit_code"`
+ LaunchError string `json:"launch_error"`
+ TimedOut bool `json:"timed_out"`
+ TraceError string `json:"trace_error"`
+ RunDirectory string `json:"run_directory"`
+ ViolatedProperties []string `json:"violated_properties"`
+}
+
+// checkerVerdict is one implementation's whole checker side, pooled across the
+// seeds it was swept at.
+type checkerVerdict struct {
+ Implementation string
+ FailedStage string
+ FailedError string
+ RunsRecorded int
+ RunsUsable int
+ ExcludedByReason map[string]int
+ FiredProperties []string
+ // SurfacesObserved holds every locatable surface seen true on at least one
+ // step of at least one usable run. A surface missing from it was never
+ // located across the whole sweep of this implementation.
+ SurfacesObserved map[string]bool
+ SurfacesKnown bool
+ TraceErrors []string
+}
+
+func (v checkerVerdict) fired() bool { return len(v.FiredProperties) > 0 }
+
+type checkerSide struct {
+ Directory string
+ SpecPath string
+ Planned []string
+ Verdicts []checkerVerdict
+}
+
+func loadChecker(directory string) (checkerSide, error) {
+ body, err := os.ReadFile(filepath.Join(directory, sweepManifestFileName))
+ if err != nil {
+ return checkerSide{}, fmt.Errorf("read %s: %w", sweepManifestFileName, err)
+ }
+ var declared sweepManifest
+ if err := json.Unmarshal(body, &declared); err != nil {
+ return checkerSide{}, fmt.Errorf("parse %s in %s: %w", sweepManifestFileName, directory, err)
+ }
+ side := checkerSide{Directory: directory, SpecPath: declared.SpecPath}
+ for _, planned := range declared.Implementations {
+ side.Planned = append(side.Planned, planned.Name)
+ }
+
+ records, err := readSweepRecords(filepath.Join(directory, sweepRecordsFileName))
+ if err != nil {
+ return checkerSide{}, err
+ }
+ for _, record := range records {
+ verdict, err := readImplementation(directory, record)
+ if err != nil {
+ return checkerSide{}, err
+ }
+ side.Verdicts = append(side.Verdicts, verdict)
+ }
+ slices.SortFunc(side.Verdicts, func(a, b checkerVerdict) int {
+ return strings.Compare(a.Implementation, b.Implementation)
+ })
+ return side, nil
+}
+
+func readSweepRecords(path string) ([]sweepImplementationRecord, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("read %s: %w", sweepRecordsFileName, err)
+ }
+ defer file.Close()
+
+ var records []sweepImplementationRecord
+ scanner := bufio.NewScanner(file)
+ scanner.Buffer(make([]byte, 0, 64*1024), maxRecordBytes)
+ lineNumber := 0
+ seen := map[string]bool{}
+ for scanner.Scan() {
+ lineNumber++
+ raw := strings.TrimSpace(scanner.Text())
+ if raw == "" {
+ continue
+ }
+ var record sweepImplementationRecord
+ if err := json.Unmarshal([]byte(raw), &record); err != nil {
+ return nil, fmt.Errorf("%s line %d: %w", sweepRecordsFileName, lineNumber, err)
+ }
+ if record.Name == "" {
+ return nil, fmt.Errorf("%s line %d names no implementation", sweepRecordsFileName, lineNumber)
+ }
+ if seen[record.Name] {
+ return nil, fmt.Errorf("%s line %d records %s a second time: its runs would be pooled twice",
+ sweepRecordsFileName, lineNumber, record.Name)
+ }
+ seen[record.Name] = true
+ records = append(records, record)
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("read %s: %w", sweepRecordsFileName, err)
+ }
+ return records, nil
+}
+
+func readImplementation(sweepDirectory string, record sweepImplementationRecord) (checkerVerdict, error) {
+ verdict := checkerVerdict{
+ Implementation: record.Name,
+ FailedStage: record.FailedStage,
+ FailedError: record.Error,
+ ExcludedByReason: map[string]int{},
+ SurfacesObserved: map[string]bool{},
+ }
+ fired := map[string]bool{}
+ for _, run := range record.Runs {
+ verdict.RunsRecorded++
+ if run.LaunchError != "" {
+ verdict.ExcludedByReason[reasonLaunchError]++
+ continue
+ }
+ directory := resolveCampaignDirectory(sweepDirectory, record.Name, run)
+ campaignRuns, err := readCampaignRuns(directory)
+ if err != nil {
+ return checkerVerdict{}, err
+ }
+ for _, campaignRun := range campaignRuns {
+ if reason := excludedBecause(campaignRun); reason != "" {
+ verdict.ExcludedByReason[reason]++
+ continue
+ }
+ verdict.RunsUsable++
+ for _, property := range campaignRun.ViolatedProperties {
+ fired[property] = true
+ }
+ observed, err := readObservedSurfaces(filepath.Join(directory, campaignRun.RunDirectory))
+ if err != nil {
+ verdict.TraceErrors = append(verdict.TraceErrors, err.Error())
+ continue
+ }
+ verdict.SurfacesKnown = true
+ for surface, seen := range observed {
+ if seen {
+ verdict.SurfacesObserved[surface] = true
+ }
+ }
+ }
+ }
+ if len(fired) > 0 {
+ verdict.FiredProperties = slices.Sorted(maps.Keys(fired))
+ }
+ if len(verdict.ExcludedByReason) == 0 {
+ verdict.ExcludedByReason = nil
+ }
+ return verdict, nil
+}
+
+// resolveCampaignDirectory prefers the path the sweep recorded and falls back to
+// the layout it names, so a sweep directory read on another machine than the one
+// that wrote its absolute paths still resolves.
+func resolveCampaignDirectory(sweepDirectory, name string, run sweepRunRecord) string {
+ if run.CampaignDirectory != "" {
+ if _, err := os.Stat(run.CampaignDirectory); err == nil {
+ return run.CampaignDirectory
+ }
+ }
+ return filepath.Join(sweepDirectory, name, "seed-"+strconv.FormatInt(run.Seed, 10))
+}
+
+func readCampaignRuns(directory string) ([]campaignRunRecord, error) {
+ if _, err := os.Stat(filepath.Join(directory, campaignManifestFileName)); err != nil {
+ return nil, fmt.Errorf("read %s in %s: %w", campaignManifestFileName, directory, err)
+ }
+ file, err := os.Open(filepath.Join(directory, campaignRecordsFileName))
+ if err != nil {
+ return nil, fmt.Errorf("read %s: %w", campaignRecordsFileName, err)
+ }
+ defer file.Close()
+
+ var records []campaignRunRecord
+ scanner := bufio.NewScanner(file)
+ scanner.Buffer(make([]byte, 0, 64*1024), maxRecordBytes)
+ lineNumber := 0
+ for scanner.Scan() {
+ lineNumber++
+ raw := strings.TrimSpace(scanner.Text())
+ if raw == "" {
+ continue
+ }
+ var record campaignRunRecord
+ if err := json.Unmarshal([]byte(raw), &record); err != nil {
+ return nil, fmt.Errorf("%s line %d in %s: %w", campaignRecordsFileName, lineNumber, directory, err)
+ }
+ records = append(records, record)
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("read %s in %s: %w", campaignRecordsFileName, directory, err)
+ }
+ return records, nil
+}
+
+func excludedBecause(record campaignRunRecord) string {
+ switch {
+ case record.LaunchError != "":
+ return reasonLaunchError
+ case record.TimedOut:
+ return reasonTimedOut
+ case record.ExitCode != 0:
+ return reasonNonzeroExit
+ case record.TraceError != "":
+ return reasonTraceError
+ default:
+ return ""
+ }
+}
+
+type traceLine struct {
+ ExtractorChanges map[string]struct {
+ Curr json.RawMessage `json:"curr"`
+ } `json:"extractor_changes"`
+}
+
+// readObservedSurfaces replays one run's locatableSurfaces readings. The
+// verifier emits every extractor as a change on the first snapshot and only on
+// a difference afterwards, so a surface true on any recorded change was located
+// at least once, and one absent from every change was never located at all.
+func readObservedSurfaces(runDirectory string) (map[string]bool, error) {
+ path := filepath.Join(runDirectory, traceFileName)
+ file, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("read %s: %w", path, err)
+ }
+ defer file.Close()
+
+ observed := map[string]bool{}
+ found := false
+ scanner := bufio.NewScanner(file)
+ scanner.Buffer(make([]byte, 0, 64*1024), maxTraceLineBytes)
+ lineNumber := 0
+ for scanner.Scan() {
+ lineNumber++
+ raw := strings.TrimSpace(scanner.Text())
+ if raw == "" {
+ continue
+ }
+ var line traceLine
+ if err := json.Unmarshal([]byte(raw), &line); err != nil {
+ return nil, fmt.Errorf("%s line %d: %w", path, lineNumber, err)
+ }
+ change, present := line.ExtractorChanges[surfacesExtractor]
+ if !present || len(change.Curr) == 0 {
+ continue
+ }
+ var reading map[string]bool
+ if err := json.Unmarshal(change.Curr, &reading); err != nil {
+ return nil, fmt.Errorf("%s line %d: %s is not an object of booleans: %w",
+ path, lineNumber, surfacesExtractor, err)
+ }
+ found = true
+ for surface, located := range reading {
+ if located {
+ observed[surface] = true
+ }
+ }
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("read %s: %w", path, err)
+ }
+ if !found {
+ return nil, fmt.Errorf("%s records no %s reading: this run cannot say whether a surface was located",
+ path, surfacesExtractor)
+ }
+ return observed, nil
+}
diff --git a/cmd/internal-tools/confusion-matrix/main.go b/cmd/internal-tools/confusion-matrix/main.go
new file mode 100644
index 0000000..471eb8e
--- /dev/null
+++ b/cmd/internal-tools/confusion-matrix/main.go
@@ -0,0 +1,111 @@
+// Command confusion-matrix cross-tabulates e4's checker verdicts against the
+// blind human review, which is the measure model-implementations.md
+// pre-registers: an implementation whose own suite passed, scored on whether a
+// property fired and on whether the reviewer found a defect.
+package main
+
+import (
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "time"
+)
+
+const usage = `confusion-matrix cross-tabulates the e4 checker against the blind human review.
+
+Usage:
+ confusion-matrix --sweep
--reviews --assignment --property-clauses [--json ]
+
+--sweep is the directory implementation-sweep wrote: sweep.json, implementations.jsonl
+and the per-implementation campaign directories under it.
+
+--reviews holds one impl-NN.md per implementation in the shape review-protocol.md
+fixes, plus any impl-NN-adjudication.md whose resolved labels replace the first
+rater's for the clauses it names.
+
+--assignment is implementations/assignment.md, the blinded implementation-to-model
+mapping, opened only after the last verdict is filed.
+
+--property-clauses declares which requirement clauses each property covers and which
+locatable surfaces it reads. Without it a fired property cannot be scored against a
+clause and a portability miss cannot be told from a clean run.
+`
+
+func run(arguments []string, stdout, stderr io.Writer) error {
+ flagSet := flag.NewFlagSet("confusion-matrix", flag.ContinueOnError)
+ flagSet.SetOutput(stderr)
+ flagSet.Usage = func() {
+ fmt.Fprint(stderr, usage)
+ flagSet.PrintDefaults()
+ }
+ var sweepDirectory string
+ var reviewsDirectory string
+ var assignmentPath string
+ var mappingPath string
+ var jsonPath string
+ flagSet.StringVar(&sweepDirectory, "sweep", "", "directory implementation-sweep wrote")
+ flagSet.StringVar(&reviewsDirectory, "reviews", "", "directory holding impl-NN.md verdict forms")
+ flagSet.StringVar(&assignmentPath, "assignment", "", "implementations/assignment.md, the implementation-to-model mapping")
+ flagSet.StringVar(&mappingPath, "property-clauses", "", "the declared property-to-clause and surface mapping")
+ flagSet.StringVar(&jsonPath, "json", "", "write the machine-readable summary here, or - for stdout")
+ if err := flagSet.Parse(arguments); err != nil {
+ return err
+ }
+ for name, value := range map[string]string{
+ "--sweep": sweepDirectory,
+ "--reviews": reviewsDirectory,
+ "--assignment": assignmentPath,
+ "--property-clauses": mappingPath,
+ } {
+ if value == "" {
+ return fmt.Errorf("%s is required", name)
+ }
+ }
+
+ mapping, err := loadMapping(mappingPath)
+ if err != nil {
+ return err
+ }
+ assignments, err := loadAssignment(assignmentPath)
+ if err != nil {
+ return err
+ }
+ checker, err := loadChecker(sweepDirectory)
+ if err != nil {
+ return err
+ }
+ reviews, err := loadReviews(reviewsDirectory)
+ if err != nil {
+ return err
+ }
+
+ result := crossTabulate(checker, reviews, assignments, mapping, time.Now().UTC())
+ writeReport(result, stdout)
+
+ if jsonPath == "" {
+ return nil
+ }
+ body, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return fmt.Errorf("marshal summary: %w", err)
+ }
+ body = append(body, '\n')
+ if jsonPath == "-" {
+ _, err = stdout.Write(body)
+ return err
+ }
+ return os.WriteFile(jsonPath, body, 0o644)
+}
+
+func main() {
+ if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
+ if errors.Is(err, flag.ErrHelp) {
+ return
+ }
+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/cmd/internal-tools/confusion-matrix/mapping.go b/cmd/internal-tools/confusion-matrix/mapping.go
new file mode 100644
index 0000000..4d21d74
--- /dev/null
+++ b/cmd/internal-tools/confusion-matrix/mapping.go
@@ -0,0 +1,232 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "regexp"
+ "slices"
+ "strings"
+)
+
+// clauseCount is R1 to R20, the twenty clauses requirement.md numbers and the
+// twenty rows review-protocol.md requires on every verdict form.
+const clauseCount = 20
+
+const (
+ surfaceUnlocatable = "unlocatable"
+ surfaceInconclusive = "inconclusive"
+)
+
+const todoMarker = "todo"
+
+// propertyMapping is one row of the property table: the clauses a property is
+// the oracle for, and the locatable surfaces it reads.
+type propertyMapping struct {
+ Property string
+ Clauses []string
+ Surfaces []string
+}
+
+// surfaceMapping says what a surface never observed across an implementation's
+// whole sweep means. Only a surface the requirement obliges every
+// implementation to show at all times can be read as unlocatable; a surface
+// that is legitimately absent when nothing is in that state is inconclusive
+// and can never mark a property unevaluated.
+type surfaceMapping struct {
+ Surface string
+ NeverObserved string
+ Note string
+}
+
+type mapping struct {
+ Path string
+ Properties []propertyMapping
+ Surfaces []surfaceMapping
+ PropertyTodoRows int
+ byProperty map[string]propertyMapping
+ coveringProperty map[string][]string
+ unlocatableSurface map[string]bool
+}
+
+var clausePattern = regexp.MustCompile(`^[Rr]([0-9]{1,2})$`)
+
+func canonicalClause(value string) (string, bool) {
+ match := clausePattern.FindStringSubmatch(strings.TrimSpace(value))
+ if match == nil {
+ return "", false
+ }
+ number := match[1]
+ trimmed := strings.TrimLeft(number, "0")
+ if trimmed == "" {
+ return "", false
+ }
+ clause := "R" + trimmed
+ if !slices.Contains(allClauses(), clause) {
+ return "", false
+ }
+ return clause, true
+}
+
+func allClauses() []string {
+ clauses := make([]string, 0, clauseCount)
+ for index := 1; index <= clauseCount; index++ {
+ clauses = append(clauses, fmt.Sprintf("R%d", index))
+ }
+ return clauses
+}
+
+func loadMapping(path string) (mapping, error) {
+ body, err := os.ReadFile(path)
+ if err != nil {
+ return mapping{}, fmt.Errorf("read property-clause mapping: %w", err)
+ }
+ result := mapping{
+ Path: path,
+ byProperty: map[string]propertyMapping{},
+ coveringProperty: map[string][]string{},
+ unlocatableSurface: map[string]bool{},
+ }
+ section := ""
+ for _, row := range parseTableRows(string(body)) {
+ head := strings.ToLower(row.cell(0))
+ switch head {
+ case "property":
+ section = "property"
+ continue
+ case "surface":
+ section = "surface"
+ continue
+ }
+ switch section {
+ case "property":
+ if err := result.addProperty(row); err != nil {
+ return mapping{}, fmt.Errorf("%s line %d: %w", path, row.Line, err)
+ }
+ case "surface":
+ if err := result.addSurface(row); err != nil {
+ return mapping{}, fmt.Errorf("%s line %d: %w", path, row.Line, err)
+ }
+ default:
+ return mapping{}, fmt.Errorf("%s line %d: table row before any header naming property or surface", path, row.Line)
+ }
+ }
+ if len(result.Surfaces) == 0 {
+ return mapping{}, fmt.Errorf("%s declares no surfaces: a portability miss cannot be told from a clean run without them", path)
+ }
+ for _, property := range result.Properties {
+ for _, surface := range property.Surfaces {
+ if _, declared := result.surfaceByName(surface); !declared {
+ return mapping{}, fmt.Errorf("%s: property %q reads surface %q, which the surface table does not declare",
+ path, property.Property, surface)
+ }
+ }
+ }
+ return result, nil
+}
+
+func (m *mapping) addProperty(row tableRow) error {
+ name := row.cell(0)
+ if name == "" {
+ return nil
+ }
+ if strings.EqualFold(name, todoMarker) {
+ m.PropertyTodoRows++
+ return nil
+ }
+ if _, seen := m.byProperty[name]; seen {
+ return fmt.Errorf("property %q is mapped twice", name)
+ }
+ entry := propertyMapping{Property: name}
+ for _, item := range splitList(row.cell(1)) {
+ if strings.EqualFold(item, todoMarker) || item == "-" || strings.EqualFold(item, "none") {
+ continue
+ }
+ clause, ok := canonicalClause(item)
+ if !ok {
+ return fmt.Errorf("property %q names clause %q, which is not one of R1 to R%d", name, item, clauseCount)
+ }
+ if slices.Contains(entry.Clauses, clause) {
+ continue
+ }
+ entry.Clauses = append(entry.Clauses, clause)
+ }
+ for _, item := range splitList(row.cell(2)) {
+ if strings.EqualFold(item, todoMarker) || item == "-" || strings.EqualFold(item, "none") {
+ continue
+ }
+ if !slices.Contains(entry.Surfaces, item) {
+ entry.Surfaces = append(entry.Surfaces, item)
+ }
+ }
+ m.Properties = append(m.Properties, entry)
+ m.byProperty[name] = entry
+ for _, clause := range entry.Clauses {
+ m.coveringProperty[clause] = append(m.coveringProperty[clause], name)
+ }
+ return nil
+}
+
+func (m *mapping) addSurface(row tableRow) error {
+ name := row.cell(0)
+ if name == "" || strings.EqualFold(name, todoMarker) {
+ return nil
+ }
+ meaning := strings.ToLower(row.cell(1))
+ if meaning != surfaceUnlocatable && meaning != surfaceInconclusive {
+ return fmt.Errorf("surface %q says %q for never observed, want %s or %s",
+ name, row.cell(1), surfaceUnlocatable, surfaceInconclusive)
+ }
+ if _, seen := m.surfaceByName(name); seen {
+ return fmt.Errorf("surface %q is declared twice", name)
+ }
+ m.Surfaces = append(m.Surfaces, surfaceMapping{Surface: name, NeverObserved: meaning, Note: row.cell(2)})
+ if meaning == surfaceUnlocatable {
+ m.unlocatableSurface[name] = true
+ }
+ return nil
+}
+
+func (m mapping) surfaceByName(name string) (surfaceMapping, bool) {
+ for _, surface := range m.Surfaces {
+ if surface.Surface == name {
+ return surface, true
+ }
+ }
+ return surfaceMapping{}, false
+}
+
+// unevaluable reports whether a property could not be evaluated against this
+// implementation because a surface it reads was never located. A surface the
+// mapping calls inconclusive never makes a property unevaluable, however many
+// steps failed to observe it.
+func (m mapping) unevaluable(property string, observed map[string]bool) bool {
+ entry, known := m.byProperty[property]
+ if !known {
+ return false
+ }
+ for _, surface := range entry.Surfaces {
+ if m.unlocatableSurface[surface] && !observed[surface] {
+ return true
+ }
+ }
+ return false
+}
+
+func (m mapping) covering(clause string) []string {
+ return m.coveringProperty[clause]
+}
+
+func (m mapping) knows(property string) bool {
+ _, known := m.byProperty[property]
+ return known
+}
+
+func (m mapping) unlocatableSurfaces() []string {
+ var names []string
+ for _, surface := range m.Surfaces {
+ if surface.NeverObserved == surfaceUnlocatable {
+ names = append(names, surface.Surface)
+ }
+ }
+ return names
+}
diff --git a/cmd/internal-tools/confusion-matrix/matrix.go b/cmd/internal-tools/confusion-matrix/matrix.go
new file mode 100644
index 0000000..8597c09
--- /dev/null
+++ b/cmd/internal-tools/confusion-matrix/matrix.go
@@ -0,0 +1,510 @@
+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
+}
diff --git a/cmd/internal-tools/confusion-matrix/report.go b/cmd/internal-tools/confusion-matrix/report.go
new file mode 100644
index 0000000..8539933
--- /dev/null
+++ b/cmd/internal-tools/confusion-matrix/report.go
@@ -0,0 +1,169 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "maps"
+ "slices"
+ "strconv"
+ "strings"
+ "text/tabwriter"
+)
+
+// labelledMatrix is one row of a printed matrix: the whole sample, or one model.
+type labelledMatrix struct {
+ Label string
+ Value clauseMatrix
+ Excluded int
+}
+
+func writeReport(outcome result, out io.Writer) {
+ fmt.Fprintln(out, "unit: implementation whose own suite passed, scored on whether a property fired and on the reviewer's overall verdict")
+ fmt.Fprintln(out, "an implementation with no cell is missing data and is listed separately, never counted as a clean run")
+ fmt.Fprintln(out)
+ writeTable(out, []string{"group", "scored", "true pos", "false pos", "false neg", "true neg", "precision", "recall", "no cell"},
+ func(add func(...string)) {
+ for _, row := range implementationRows(outcome) {
+ add(
+ row.Label,
+ strconv.Itoa(row.Value.Scored),
+ strconv.Itoa(row.Value.TruePositive),
+ strconv.Itoa(row.Value.FalsePositive),
+ strconv.Itoa(row.Value.FalseNegative),
+ strconv.Itoa(row.Value.TrueNegative),
+ formatRatio(row.Value.Precision),
+ formatRatio(row.Value.Recall),
+ strconv.Itoa(row.Excluded),
+ )
+ }
+ })
+
+ fmt.Fprintln(out)
+ fmt.Fprintln(out, "clause pairs are an implementation against one of R1 to R20, scored through the declared property-to-clause mapping")
+ fmt.Fprintln(out, "cannot tell is the reviewer's specification-error answer and is evidence neither way")
+ fmt.Fprintln(out, "unevaluated is a clause whose every covering property a never-located surface left unrunnable: a portability miss, not a cell")
+ writeTable(out, []string{"group", "scored", "true pos", "false pos", "false neg", "true neg",
+ "precision", "recall", "cannot tell", "unevaluated", "uncovered", "uncovered false neg"},
+ func(add func(...string)) {
+ for _, row := range clauseRows(outcome) {
+ add(
+ row.Label,
+ strconv.Itoa(row.Value.Scored),
+ strconv.Itoa(row.Value.TruePositive),
+ strconv.Itoa(row.Value.FalsePositive),
+ strconv.Itoa(row.Value.FalseNegative),
+ strconv.Itoa(row.Value.TrueNegative),
+ formatRatio(row.Value.Precision),
+ formatRatio(row.Value.Recall),
+ strconv.Itoa(row.Value.CannotTell),
+ strconv.Itoa(row.Value.UnevaluatedSurfaceMissed),
+ strconv.Itoa(row.Value.UncoveredScored),
+ strconv.Itoa(row.Value.UncoveredFalseNegative),
+ )
+ }
+ })
+
+ fmt.Fprintln(out)
+ writeTable(out, []string{"implementation", "model", "cell", "usable runs", "fired", "violated clauses", "cannot tell"},
+ func(add func(...string)) {
+ for _, row := range outcome.Outcomes {
+ add(
+ row.Implementation,
+ row.Model,
+ row.Cell,
+ strconv.Itoa(row.RunsUsable),
+ joinOrDash(row.FiredProperties),
+ joinOrDash(row.ViolatedClauses),
+ strconv.Itoa(len(row.CannotTellClauses)),
+ )
+ }
+ })
+
+ if len(outcome.Excluded) > 0 {
+ fmt.Fprintf(out, "\n%d implementation(s) carry no cell: missing data, never a clean run\n", len(outcome.Excluded))
+ writeTable(out, []string{"implementation", "model", "reason", "detail"}, func(add func(...string)) {
+ for _, entry := range outcome.Excluded {
+ add(entry.Implementation, orDash(entry.Model), entry.Reason, orDash(entry.Detail))
+ }
+ })
+ }
+
+ fmt.Fprintf(out, "\nspecification portability over the %d scored implementation(s): %d needed a locating adaptation\n",
+ outcome.Portability.Scored, outcome.Portability.WithUnlocatableSurface)
+ if len(outcome.Portability.BySurface) > 0 {
+ writeTable(out, []string{"surface never located", "implementations"}, func(add func(...string)) {
+ for _, surface := range slices.Sorted(maps.Keys(outcome.Portability.BySurface)) {
+ add(surface, strconv.Itoa(outcome.Portability.BySurface[surface]))
+ }
+ })
+ }
+ if len(outcome.Portability.SurfacesReadAsInconclusive) > 0 {
+ fmt.Fprintf(out, "a miss on %s cannot be told from that surface being legitimately absent, so neither counts against portability\n",
+ strings.Join(outcome.Portability.SurfacesReadAsInconclusive, ", "))
+ }
+
+ fmt.Fprintf(out, "\nclause coverage: %d mapped propert(ies) cover %d of %d clauses\n",
+ outcome.Coverage.MappedProperties, len(outcome.Coverage.ClausesCovered), clauseCount)
+ if len(outcome.Coverage.ClausesUncovered) > 0 {
+ fmt.Fprintf(out, "no property covers %s\n", strings.Join(outcome.Coverage.ClausesUncovered, ", "))
+ }
+ fmt.Fprintf(out, "review cost over the scored implementations: %d minutes\n", outcome.ReviewMinutes)
+
+ for _, note := range outcome.Notes {
+ fmt.Fprintf(out, "\nnote: %s\n", note)
+ }
+}
+
+func implementationRows(outcome result) []labelledMatrix {
+ rows := []labelledMatrix{{
+ Label: "all",
+ Value: clauseMatrix{matrix: outcome.Implementations},
+ Excluded: len(outcome.Excluded),
+ }}
+ for _, model := range outcome.ByModel {
+ rows = append(rows, labelledMatrix{
+ Label: model.Model,
+ Value: clauseMatrix{matrix: model.Implementations},
+ Excluded: model.Excluded,
+ })
+ }
+ return rows
+}
+
+func clauseRows(outcome result) []labelledMatrix {
+ rows := []labelledMatrix{{Label: "all", Value: outcome.Clauses}}
+ for _, model := range outcome.ByModel {
+ rows = append(rows, labelledMatrix{Label: model.Model, Value: model.Clauses})
+ }
+ return rows
+}
+
+func writeTable(out io.Writer, header []string, rows func(add func(...string))) {
+ writer := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
+ fmt.Fprintln(writer, strings.Join(header, "\t"))
+ rows(func(cells ...string) {
+ fmt.Fprintln(writer, strings.Join(cells, "\t"))
+ })
+ writer.Flush()
+}
+
+func formatRatio(value *float64) string {
+ if value == nil {
+ return "n/a"
+ }
+ return strconv.FormatFloat(*value, 'f', 3, 64)
+}
+
+func joinOrDash(values []string) string {
+ if len(values) == 0 {
+ return "-"
+ }
+ return strings.Join(values, " ")
+}
+
+func orDash(value string) string {
+ if value == "" {
+ return "-"
+ }
+ return value
+}
diff --git a/cmd/internal-tools/confusion-matrix/review.go b/cmd/internal-tools/confusion-matrix/review.go
new file mode 100644
index 0000000..6f8329c
--- /dev/null
+++ b/cmd/internal-tools/confusion-matrix/review.go
@@ -0,0 +1,237 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "slices"
+ "strconv"
+ "strings"
+)
+
+const (
+ clauseMeets = "meets"
+ clauseViolates = "violates"
+ clauseCannotTell = "cannot tell"
+)
+
+const (
+ overallDefective = "defective"
+ overallNotDefective = "not defective"
+)
+
+// reviewVerdict is one filed verdict form: the twenty clause rows and the
+// overall verdict review-protocol.md requires, with any adjudicated label
+// already substituted for the first rater's.
+type reviewVerdict struct {
+ Implementation string
+ Path string
+ Reviewer string
+ Date string
+ Minutes int
+ Overall string
+ Clauses map[string]string
+ Adjudicated []string
+}
+
+func (v reviewVerdict) defective() bool { return v.Overall == overallDefective }
+
+func (v reviewVerdict) violatedClauses() []string {
+ var violated []string
+ for _, clause := range allClauses() {
+ if v.Clauses[clause] == clauseViolates {
+ violated = append(violated, clause)
+ }
+ }
+ return violated
+}
+
+type malformedReview struct {
+ Implementation string
+ Path string
+ Reason string
+}
+
+type reviewSide struct {
+ Directory string
+ Verdicts []reviewVerdict
+ Malformed []malformedReview
+}
+
+var (
+ reviewFileName = regexp.MustCompile(`^(impl-\d+)\.md$`)
+ adjudicationName = regexp.MustCompile(`^(impl-\d+)-adjudication\.md$`)
+ secondRaterName = regexp.MustCompile(`^impl-\d+-r2\.md$`)
+ keyValueLine = regexp.MustCompile(`(?i)^\s*[-*]?\s*(reviewer|date|minutes|overall verdict|overall)\s*:\s*(.+?)\s*$`)
+ leadingWholeNumbers = regexp.MustCompile(`^\d+`)
+)
+
+func loadReviews(directory string) (reviewSide, error) {
+ entries, err := os.ReadDir(directory)
+ if err != nil {
+ return reviewSide{}, fmt.Errorf("read reviews: %w", err)
+ }
+ side := reviewSide{Directory: directory}
+ adjudications := map[string]map[string]string{}
+ var forms []struct {
+ name string
+ path string
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ path := filepath.Join(directory, entry.Name())
+ switch {
+ case secondRaterName.MatchString(entry.Name()):
+ continue
+ case adjudicationName.MatchString(entry.Name()):
+ name := adjudicationName.FindStringSubmatch(entry.Name())[1]
+ resolved, err := readAdjudication(path)
+ if err != nil {
+ side.Malformed = append(side.Malformed, malformedReview{
+ Implementation: name, Path: path, Reason: err.Error(),
+ })
+ continue
+ }
+ adjudications[name] = resolved
+ case reviewFileName.MatchString(entry.Name()):
+ forms = append(forms, struct {
+ name string
+ path string
+ }{reviewFileName.FindStringSubmatch(entry.Name())[1], path})
+ }
+ }
+
+ for _, form := range forms {
+ verdict, err := readReview(form.name, form.path)
+ if err != nil {
+ side.Malformed = append(side.Malformed, malformedReview{
+ Implementation: form.name, Path: form.path, Reason: err.Error(),
+ })
+ continue
+ }
+ for clause, label := range adjudications[form.name] {
+ if verdict.Clauses[clause] != label {
+ verdict.Adjudicated = append(verdict.Adjudicated, clause)
+ }
+ verdict.Clauses[clause] = label
+ }
+ slices.Sort(verdict.Adjudicated)
+ side.Verdicts = append(side.Verdicts, verdict)
+ }
+ slices.SortFunc(side.Verdicts, func(a, b reviewVerdict) int {
+ return strings.Compare(a.Implementation, b.Implementation)
+ })
+ slices.SortFunc(side.Malformed, func(a, b malformedReview) int {
+ return strings.Compare(a.Path, b.Path)
+ })
+ return side, nil
+}
+
+func readReview(name, path string) (reviewVerdict, error) {
+ body, err := os.ReadFile(path)
+ if err != nil {
+ return reviewVerdict{}, err
+ }
+ verdict := reviewVerdict{Implementation: name, Path: path, Clauses: map[string]string{}}
+ for _, line := range strings.Split(string(body), "\n") {
+ match := keyValueLine.FindStringSubmatch(normalizeCell(line))
+ if match == nil {
+ continue
+ }
+ value := strings.TrimSpace(match[2])
+ switch strings.ToLower(match[1]) {
+ case "reviewer":
+ verdict.Reviewer = value
+ case "date":
+ verdict.Date = value
+ case "minutes":
+ if digits := leadingWholeNumbers.FindString(value); digits != "" {
+ verdict.Minutes, _ = strconv.Atoi(digits)
+ }
+ case "overall", "overall verdict":
+ overall, ok := canonicalOverall(value)
+ if !ok {
+ return reviewVerdict{}, fmt.Errorf("overall verdict %q is neither %s nor %s", value, overallDefective, overallNotDefective)
+ }
+ verdict.Overall = overall
+ }
+ }
+ clauses, err := readClauseRows(string(body))
+ if err != nil {
+ return reviewVerdict{}, err
+ }
+ verdict.Clauses = clauses
+ for _, clause := range allClauses() {
+ if _, filed := verdict.Clauses[clause]; !filed {
+ return reviewVerdict{}, fmt.Errorf("no row for clause %s: the form must carry all %d", clause, clauseCount)
+ }
+ }
+ if verdict.Overall == "" {
+ return reviewVerdict{}, fmt.Errorf("no overall verdict: the matrix scores the reviewer's own %s or %s", overallDefective, overallNotDefective)
+ }
+ return verdict, nil
+}
+
+func readClauseRows(body string) (map[string]string, error) {
+ clauses := map[string]string{}
+ for _, row := range parseTableRows(body) {
+ clause, ok := canonicalClause(row.cell(0))
+ if !ok {
+ continue
+ }
+ label, ok := canonicalClauseVerdict(row.cell(1))
+ if !ok {
+ return nil, fmt.Errorf("clause %s has verdict %q, want %s, %s or %s",
+ clause, row.cell(1), clauseMeets, clauseViolates, clauseCannotTell)
+ }
+ if existing, seen := clauses[clause]; seen && existing != label {
+ return nil, fmt.Errorf("clause %s is filed twice, as %q and %q", clause, existing, label)
+ }
+ clauses[clause] = label
+ }
+ return clauses, nil
+}
+
+func readAdjudication(path string) (map[string]string, error) {
+ body, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ resolved, err := readClauseRows(string(body))
+ if err != nil {
+ return nil, err
+ }
+ if len(resolved) == 0 {
+ return nil, fmt.Errorf("no resolved clause rows")
+ }
+ return resolved, nil
+}
+
+func canonicalClauseVerdict(value string) (string, bool) {
+ switch strings.ToLower(strings.TrimSpace(value)) {
+ case clauseMeets, "meet", "met":
+ return clauseMeets, true
+ case clauseViolates, "violate", "violated":
+ return clauseViolates, true
+ case clauseCannotTell, "cannot-tell", "cannot_tell", "can't tell", "cant tell":
+ return clauseCannotTell, true
+ default:
+ return "", false
+ }
+}
+
+func canonicalOverall(value string) (string, bool) {
+ cleaned := strings.ToLower(strings.TrimSpace(value))
+ cleaned = strings.TrimSuffix(cleaned, ".")
+ switch cleaned {
+ case overallDefective:
+ return overallDefective, true
+ case overallNotDefective, "not-defective", "no defect", "clean":
+ return overallNotDefective, true
+ default:
+ return "", false
+ }
+}
diff --git a/cmd/internal-tools/confusion-matrix/table.go b/cmd/internal-tools/confusion-matrix/table.go
new file mode 100644
index 0000000..6b5cbd2
--- /dev/null
+++ b/cmd/internal-tools/confusion-matrix/table.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "regexp"
+ "strings"
+)
+
+// tableRow is one pipe-delimited markdown row with its 1-based line number, so
+// a malformed cell can name the line the author has to go and fix.
+type tableRow struct {
+ Line int
+ Cells []string
+}
+
+var separatorCell = regexp.MustCompile(`^:?-{2,}:?$`)
+
+func parseTableRows(body string) []tableRow {
+ var rows []tableRow
+ for index, raw := range strings.Split(body, "\n") {
+ line := strings.TrimSpace(raw)
+ if !strings.HasPrefix(line, "|") {
+ continue
+ }
+ cells := splitCells(line)
+ if len(cells) == 0 || isSeparatorRow(cells) {
+ continue
+ }
+ rows = append(rows, tableRow{Line: index + 1, Cells: cells})
+ }
+ return rows
+}
+
+func splitCells(line string) []string {
+ trimmed := strings.Trim(line, "|")
+ parts := strings.Split(trimmed, "|")
+ cells := make([]string, 0, len(parts))
+ for _, part := range parts {
+ cells = append(cells, normalizeCell(part))
+ }
+ return cells
+}
+
+func isSeparatorRow(cells []string) bool {
+ for _, cell := range cells {
+ if !separatorCell.MatchString(cell) {
+ return false
+ }
+ }
+ return true
+}
+
+func normalizeCell(value string) string {
+ cleaned := strings.ReplaceAll(value, "**", "")
+ cleaned = strings.ReplaceAll(cleaned, "`", "")
+ return strings.Join(strings.Fields(cleaned), " ")
+}
+
+func (row tableRow) cell(index int) string {
+ if index >= len(row.Cells) {
+ return ""
+ }
+ return row.Cells[index]
+}
+
+var listSeparators = regexp.MustCompile(`[,\s]+`)
+
+func splitList(value string) []string {
+ trimmed := strings.TrimSpace(value)
+ if trimmed == "" {
+ return nil
+ }
+ var items []string
+ for _, item := range listSeparators.Split(trimmed, -1) {
+ if item != "" {
+ items = append(items, item)
+ }
+ }
+ return items
+}