From 9046052a6a6f21a42f7bfdf6d2887a0dded1f98b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 22:49:24 -0400 Subject: [PATCH 1/4] pool: a test holds what show and verify say about both metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The index declares a second metric — the graded shares beside the judged scores — and the reading forms counted it. The tests hold the line shape: one line per metric after the index line, the metric names in verify's sentence, and a metric_list array beside the count the --json answers already carried. The index's Unit and Dims accessors are held here too. Co-Authored-By: codeaf --- cmd/codeaf/pool_test.go | 182 +++++++++++++++++++++++++++++- internal/pool/index/index_test.go | 38 +++++++ 2 files changed, 219 insertions(+), 1 deletion(-) diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index e369c05df..f4131163e 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -72,6 +72,30 @@ func poolDoc() []byte { }`) } +// poolDocBothMetrics is the relay's wire shape: the judge's scored cells and +// the graded shares beside them, the shares split by the source that +// produced each. The numbers name no model; it is a fixture, and the field +// it is read for is its shape. +func poolDocBothMetrics() []byte { + return []byte(`{ + "version": 7, + "schema": 1, + "generated": "2026-09-10", + "min_installs": 1, + "judges": ["z-ai/glm-5.3"], + "metrics": { + "role_quality": {"kind": "gaussian", "unit": "score", "dims": ["role", "model"]}, + "acceptable": {"kind": "bernoulli", "unit": "share", "dims": ["role", "model", "source"]} + }, + "cells": [ + {"metric": "role_quality", "role": "worker", "model": "z-ai/glm-5.3", "mean": 75, "sd": 7, "n": 30}, + {"metric": "acceptable", "role": "worker", "model": "z-ai/glm-5.3", "mean": 0.9, "sd": 0, "n": 20, "source": "reviewer"}, + {"metric": "acceptable", "role": "planner", "model": "z-ai/glm-5.3", "mean": 0.8, "sd": 0, "n": 30, "source": "reviewer"}, + {"metric": "acceptable", "role": "worker", "model": "z-ai/glm-5.3", "mean": 0.7, "sd": 0, "n": 15, "source": "grader"} + ] + }`) +} + // seedIndex writes poolDoc where show reads the cache, and nothing else: // show takes the document as it stands and does not ask its signature — // that is verify's question, not the reading form's. @@ -595,7 +619,7 @@ func TestPoolVerifyFetchesAndChecksASignedIndex(t *testing.T) { &out, dir, poolClock(t), lookup); err != nil { t.Fatal(err) } - if !strings.Contains(out.String(), "signature good: version 7, generated 2026-09-10, 1 metric") { + if !strings.Contains(out.String(), "signature good: version 7, generated 2026-09-10, metrics role_rating") { t.Fatalf("verify did not read the fetched document:\n%s", out.String()) } out.Reset() @@ -607,6 +631,71 @@ func TestPoolVerifyFetchesAndChecksASignedIndex(t *testing.T) { } } +// verify names the metrics it verified, where it used to count them: the +// count said how many, the names say which, and the second metric is no +// longer invisible. --json carries the array beside the count the way the +// reading forms do. +func TestPoolVerifyNamesTheMetricsItVerified(t *testing.T) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + src := t.TempDir() + doc := poolDocBothMetrics() + if err := os.WriteFile(filepath.Join(src, "index.json"), doc, 0o644); err != nil { + t.Fatal(err) + } + sig := ed25519.Sign(priv, doc) + if err := os.WriteFile(filepath.Join(src, "index.json.sig"), + []byte(base64.StdEncoding.EncodeToString(sig)), 0o644); err != nil { + t.Fatal(err) + } + lookup := oneEnv("CODEAF_MODEL_POOL_URL", filepath.Join(src, "index.json")) + dir := t.TempDir() + var out strings.Builder + if err := runPoolWith([]string{"verify", "--key", base64.StdEncoding.EncodeToString(pub)}, + &out, dir, poolClock(t), lookup); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "signature good: version 7, generated 2026-09-10, metrics acceptable, role_quality") { + t.Fatalf("verify did not name both metrics:\n%s", out.String()) + } + + out.Reset() + if err := runPoolWith([]string{"verify", "--json", "--key", base64.StdEncoding.EncodeToString(pub)}, + &out, dir, poolClock(t), lookup); err != nil { + t.Fatal(err) + } + var answer struct { + Verified bool `json:"verified"` + Metrics int `json:"metrics"` + MetricList []struct { + Name string `json:"name"` + Kind string `json:"kind"` + Unit string `json:"unit"` + Dims []string `json:"dims"` + Cells int `json:"cells"` + Sources []string `json:"sources"` + } `json:"metric_list"` + } + if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { + t.Fatalf("verify --json did not parse: %v\n%s", err, out.String()) + } + if !answer.Verified || answer.Metrics != 2 || len(answer.MetricList) != 2 { + t.Fatalf("verify reported %+v, want both metrics beside the count", answer) + } + shares, quality := answer.MetricList[0], answer.MetricList[1] + if shares.Name != "acceptable" || shares.Kind != "bernoulli" || shares.Unit != "share" || + strings.Join(shares.Dims, ",") != "role,model,source" || shares.Cells != 3 || + strings.Join(shares.Sources, ",") != "grader,reviewer" { + t.Fatalf("the graded shares read as %+v", shares) + } + if quality.Name != "role_quality" || quality.Kind != "gaussian" || quality.Unit != "score" || + strings.Join(quality.Dims, ",") != "role,model" || quality.Cells != 1 || len(quality.Sources) != 0 { + t.Fatalf("the judged scores read as %+v", quality) + } +} + // A document the key does not trust is not verified: the failure says so in // the puller's words on exit 1, and — this is the puller's own promise, read // through the door — nothing wrong is cached in its place. @@ -871,6 +960,97 @@ func TestPoolShowSaysHowManyCellsTheBuiltInSeedHolds(t *testing.T) { if want := countWord(total, "cell", "cells"); !strings.Contains(out.String(), want) { t.Errorf("show did not say the seed holds %q:\n%s", want, out.String()) } + // The seed's one metric says itself the way a cached document's do: + // the kind and unit the document spells, its cells, and the dims a cell + // of it is addressed by. + if want := "role_quality: gaussian score · " + countWord(total, "cell", "cells") + " · dims role, model"; !strings.Contains(out.String(), want) { + t.Errorf("the seed's metric line did not read %q:\n%s", want, out.String()) + } +} + +// The index declares two metrics now — the judge's scores and the graded +// shares beside them — and a count says neither which nor what. show prints +// one line per declared metric after the index line: the kind and unit the +// document spells, the cells counted with their noun, the dims a cell of +// the metric is addressed by, and the distinct sources when the cells are +// split by one. The order is the index's own, sorted. +func TestPoolShowPrintsEachMetricAfterTheIndexLine(t *testing.T) { + dir := t.TempDir() + writePoolDoc(t, dir, string(poolDocBothMetrics())) + var out strings.Builder + if err := runPoolWith([]string{"show"}, &out, dir, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + body := out.String() + indexAt := strings.Index(body, "index · ") + qualityAt := strings.Index(body, "role_quality: gaussian score · 1 cell · dims role, model") + sharesAt := strings.Index(body, "acceptable: bernoulli share · 3 cells · dims role, model, source · sources grader, reviewer") + if indexAt < 0 || qualityAt < 0 || sharesAt < 0 { + t.Fatalf("show did not print both metrics beside the index line:\n%s", body) + } + if qualityAt < indexAt || sharesAt < qualityAt { + t.Fatalf("the metric lines did not follow the index line in the index's own order:\n%s", body) + } +} + +// A document that declares one metric prints one line, and a metric the +// document spells no unit for says its kind alone. +func TestPoolShowPrintsOneLineForAOneMetricDocument(t *testing.T) { + var out strings.Builder + if err := runPoolWith([]string{"show"}, &out, seedIndex(t), poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + if !strings.Contains(out.String(), "role_rating: gaussian · 1 cell · dims role, model") { + t.Fatalf("the one-metric document did not print its one line:\n%s", out.String()) + } +} + +// The --json answer carries the metrics as an array beside the count it +// already carried, so a script written against today's shape still reads +// and a script that wants the split reads it from the array. +func TestPoolShowJSONCarriesTheMetricsBesideTheCount(t *testing.T) { + dir := t.TempDir() + writePoolDoc(t, dir, string(poolDocBothMetrics())) + var out strings.Builder + if err := runPoolWith([]string{"show", "--json"}, &out, dir, poolClock(t), noEnv); err != nil { + t.Fatal(err) + } + var answer struct { + Index *struct { + Metrics int `json:"metrics"` + MetricList []struct { + Name string `json:"name"` + Kind string `json:"kind"` + Unit string `json:"unit"` + Dims []string `json:"dims"` + Cells int `json:"cells"` + Sources []string `json:"sources"` + } `json:"metric_list"` + } `json:"index"` + } + if err := json.Unmarshal([]byte(out.String()), &answer); err != nil { + t.Fatalf("--json did not parse: %v\n%s", err, out.String()) + } + held := answer.Index + if held == nil { + t.Fatal("a seeded index was not reported") + } + if held.Metrics != 2 { + t.Fatalf("the count did not stay a count: %d", held.Metrics) + } + if len(held.MetricList) != 2 { + t.Fatalf("the array carried %d metric(s), want both: %+v", len(held.MetricList), held.MetricList) + } + shares, quality := held.MetricList[0], held.MetricList[1] + if shares.Name != "acceptable" || shares.Kind != "bernoulli" || shares.Unit != "share" || + strings.Join(shares.Dims, ",") != "role,model,source" || shares.Cells != 3 || + strings.Join(shares.Sources, ",") != "grader,reviewer" { + t.Fatalf("the graded shares read as %+v", shares) + } + if quality.Name != "role_quality" || quality.Kind != "gaussian" || quality.Unit != "score" || + strings.Join(quality.Dims, ",") != "role,model" || quality.Cells != 1 || len(quality.Sources) != 0 { + t.Fatalf("the judged scores read as %+v", quality) + } } // ── THE OWN SHEET ─────────────────────────────────────────────────────────── diff --git a/internal/pool/index/index_test.go b/internal/pool/index/index_test.go index 2b2b10b1f..4c9986d0c 100644 --- a/internal/pool/index/index_test.go +++ b/internal/pool/index/index_test.go @@ -59,6 +59,9 @@ func TestTheDocumentReadsBack(t *testing.T) { if kind, ok := x.Kind("role_rating"); !ok || kind != "gaussian" { t.Errorf("kind is %q, %v", kind, ok) } + if got := x.Unit("role_rating"); got != "elo" { + t.Errorf("unit is %q, want elo", got) + } c, ok := x.Cell("role_rating", "planner", "z-ai/glm-5.3", nil) if !ok { t.Fatal("the example cell was not found") @@ -191,6 +194,41 @@ func TestMetricsAreSortedAndCellsOfUndeclaredMetricsAreSkipped(t *testing.T) { } } +func TestMetricUnitAndDeclaredDimsReadBack(t *testing.T) { + x := mustParse(t, `{ + "metrics": { + "role_quality": {"kind": "gaussian", "unit": "score", "dims": ["role", "model"]}, + "acceptable": {"kind": "bernoulli", "unit": "share", "dims": ["source", "role", "model"]}, + "tiered": {"kind": "a", "dims": ["tier", "region"]}, + "bare": {"kind": "a"} + } + }`) + if got := x.Unit("role_quality"); got != "score" { + t.Errorf("role_quality's unit is %q, want score", got) + } + if got := x.Unit(" ACCEPTABLE "); got != "share" { + t.Errorf("a unit lookup did not fold the metric name: %q", got) + } + if got := x.Unit("bare"); got != "" { + t.Errorf("a metric with no unit answered %q", got) + } + if got := x.Unit("undeclared"); got != "" { + t.Errorf("an undeclared metric's unit is %q, want empty", got) + } + if got := strings.Join(x.Dims("acceptable"), ","); got != "source" { + t.Errorf("acceptable's dims are %q, want source alone", got) + } + if got := strings.Join(x.Dims("tiered"), ","); got != "region,tier" { + t.Errorf("tiered's dims are %q, want region,tier sorted", got) + } + if got := x.Dims("role_quality"); len(got) != 0 { + t.Errorf("role_quality answered dims %v, want none beyond role and model", got) + } + if got := x.Dims("undeclared"); len(got) != 0 { + t.Errorf("an undeclared metric answered dims %v", got) + } +} + func TestDeclaredDimsBecomeCellDims(t *testing.T) { x := mustParse(t, `{ "metrics": {"m": {"kind": "a", "dims": ["role", "model", "region", "variant"]}}, From 561d37adc878d782bf5d5ee0cbab170070c413bb Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 22:52:45 -0400 Subject: [PATCH 2/4] pool: show and verify print both metrics the index carries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The index declares the graded shares beside the judged scores, and the reading forms counted them: a person could not tell which cells are judged scores and which are graded shares. show prints one line per declared metric after the index line — the kind and unit the document spells, the cells counted, the dims a cell is addressed by, and the distinct sources when the cells are split by one — and verify names the metrics where it counted them. The --json answers carry a metric_list array beside the count, which stays a count so no reader of today's shape breaks. The index's Unit and Dims accessors answer the words the document spells; the seed's one metric prints one line. Co-Authored-By: codeaf --- cmd/codeaf/pool.go | 164 +++++++++++++++++++++++++++++++---- cmd/codeaf/pool_test.go | 6 +- internal/pool/index/index.go | 31 ++++++- 3 files changed, 180 insertions(+), 21 deletions(-) diff --git a/cmd/codeaf/pool.go b/cmd/codeaf/pool.go index 65bb36ef9..86d4c7ad2 100644 --- a/cmd/codeaf/pool.go +++ b/cmd/codeaf/pool.go @@ -22,6 +22,7 @@ import ( "io" "os" "path/filepath" + "sort" "strings" "time" @@ -209,7 +210,8 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti return err } } - if cached == nil { + held := cached + if held == nil { // A nothing is said in a sentence, the way an empty cache is: // silence and a bare header both read as a command that broke. And the // index the build carries is named beside it, so a person knows there @@ -218,6 +220,7 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti line := "no index cached yet" if seed, err := index.SeedIndex(); err == nil { line = fmt.Sprintf("no index cached yet · built-in seed of %s, %s", seed.Generated().Format("2006-01-02"), countWord(indexCellCount(seed), "cell", "cells")) + held = seed } if _, err := fmt.Fprintln(output, line); err != nil { return err @@ -232,6 +235,14 @@ func printPool(output io.Writer, poolDir string, cfg poolcfg.Config, now time.Ti return err } } + // One line per declared metric, after the index line: the count above + // says how many, these say which — and which of them are judged scores + // and which are graded shares. + for _, line := range metricLines(held) { + if _, err := fmt.Fprintln(output, line); err != nil { + return err + } + } // The install's own sheet is said the way every other nothing here is // said: a sheet that holds no cell yet is `none`, and one that holds some // is counted with its noun. @@ -415,14 +426,31 @@ type poolAnswer struct { // "cache" for the one under the profile, "seed" for the one the build carries // when there is no cache. type indexSummary struct { - Generated string `json:"generated"` - AgeSeconds int `json:"age_seconds"` - Schema int `json:"schema"` - Metrics int `json:"metrics"` - Judges int `json:"judges"` - Cells int `json:"cells"` - MinInstalls int `json:"min_installs"` - Source string `json:"source"` + Generated string `json:"generated"` + AgeSeconds int `json:"age_seconds"` + Schema int `json:"schema"` + // Metrics stays the count readers of today's shape already read; + // metric_list is the per-metric detail beside it, not instead of it. + Metrics int `json:"metrics"` + MetricList []metricSummary `json:"metric_list"` + Judges int `json:"judges"` + Cells int `json:"cells"` + MinInstalls int `json:"min_installs"` + Source string `json:"source"` +} + +// metricSummary is one declared metric as the reading forms carry it: the +// words the document spells for it, its cells counted, and — when its cells +// are split by the source that produced each measurement — the distinct +// sources beside them. A metric whose cells are not split by one carries no +// sources. +type metricSummary struct { + Name string `json:"name"` + Kind string `json:"kind"` + Unit string `json:"unit"` + Dims []string `json:"dims"` + Cells int `json:"cells"` + Sources []string `json:"sources,omitempty"` } func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached *index.Index, now time.Time, withStatus bool, keys []ed25519.PublicKey) error { @@ -451,6 +479,7 @@ func printPoolJSON(output io.Writer, poolDir string, cfg poolcfg.Config, cached AgeSeconds: int(now.Sub(generated) / time.Second), Schema: held.Schema(), Metrics: len(held.Metrics()), + MetricList: indexMetricSummaries(held), Judges: len(held.Judges()), Cells: indexCellCount(held), MinInstalls: held.MinInstalls(), @@ -563,19 +592,27 @@ func verifyPool(args []string, output io.Writer, poolDir string, cfg poolcfg.Con generated := held.Generated().Format("2006-01-02") if *asJSON { encoded, err := json.Marshal(struct { - Verified bool `json:"verified"` - Version int64 `json:"version"` - Generated string `json:"generated"` - Metrics int `json:"metrics"` - }{true, result.Version, generated, len(held.Metrics())}) + Verified bool `json:"verified"` + Version int64 `json:"version"` + Generated string `json:"generated"` + Metrics int `json:"metrics"` + MetricList []metricSummary `json:"metric_list"` + }{true, result.Version, generated, len(held.Metrics()), indexMetricSummaries(held)}) if err != nil { return err } _, err = fmt.Fprintf(output, "%s\n", encoded) return err } - _, err = fmt.Fprintf(output, "signature good: version %d, generated %s, %d metrics\n", - result.Version, generated, len(held.Metrics())) + // The names where the count was: a count said how many, the names say + // which. A document that declares none still says so, with the count's + // own word for a nothing. + said := "none" + if names := held.Metrics(); len(names) > 0 { + said = strings.Join(names, ", ") + } + _, err = fmt.Fprintf(output, "signature good: version %d, generated %s, metrics %s\n", + result.Version, generated, said) return err } @@ -652,6 +689,101 @@ func indexCellCount(held *index.Index) int { return total } +// indexMetricSummaries is one summary per declared metric, in the index's +// sorted order: the words the document spells for it, its cells counted, +// and the distinct sources gathered off the cells of a metric whose cells +// are split by one. The reader keeps the spellings a cell carried, so a +// dim key and a source value are matched and said the way the index folds +// a name — lowercased, trimmed. +func indexMetricSummaries(held *index.Index) []metricSummary { + names := held.Metrics() + out := make([]metricSummary, 0, len(names)) + for _, name := range names { + kind, _ := held.Kind(name) + summary := metricSummary{ + Name: name, + Kind: kind, + Unit: held.Unit(name), + Dims: append([]string{"role", "model"}, held.Dims(name)...), + Cells: len(held.Cells(name)), + } + seen := map[string]bool{} + for _, cell := range held.Cells(name) { + source, spelled := cellDim(cell, "source") + if !spelled { + continue + } + folded := poolFold(source) + if folded != "" && !seen[folded] { + seen[folded] = true + summary.Sources = append(summary.Sources, folded) + } + } + sort.Strings(summary.Sources) + out = append(out, summary) + } + return out +} + +// metricLines is one line per metric the held document declares, in the +// index's sorted order. A nothing answers nothing, the way every other +// reading form reads what a person has. +func metricLines(held *index.Index) []string { + if held == nil { + return nil + } + summaries := indexMetricSummaries(held) + lines := make([]string, 0, len(summaries)) + for _, summary := range summaries { + lines = append(lines, metricLine(summary)) + } + return lines +} + +// metricLine is one declared metric said on one line: `role_quality: +// gaussian score · 12 cells · dims role, model` — the kind and unit the +// document spells (an absent word is left out rather than printed empty), +// the cells counted with their noun, the dims a cell of the metric is +// addressed by, and the distinct sources beside them when the cells are +// split by one. +func metricLine(summary metricSummary) string { + parts := make([]string, 0, 4) + if summary.Kind != "" || summary.Unit != "" { + words := make([]string, 0, 2) + if summary.Kind != "" { + words = append(words, summary.Kind) + } + if summary.Unit != "" { + words = append(words, summary.Unit) + } + parts = append(parts, strings.Join(words, " ")) + } + parts = append(parts, countWord(summary.Cells, "cell", "cells"), + "dims "+strings.Join(summary.Dims, ", ")) + if len(summary.Sources) > 0 { + parts = append(parts, "sources "+strings.Join(summary.Sources, ", ")) + } + return summary.Name + ": " + strings.Join(parts, " · ") +} + +// poolFold is the way the index matches a name or a value — lowercased and +// trimmed — spelled here because the reader keeps the spellings a cell +// carried and the display says what matches. +func poolFold(word string) string { + return strings.ToLower(strings.TrimSpace(word)) +} + +// cellDim reads one dim off a cell, under the way the index matches a dim +// key: the declared spelling wins over the case the cell happened to spell. +func cellDim(cell index.Cell, dim string) (string, bool) { + for key, value := range cell.Dims { + if poolFold(key) == dim { + return value, true + } + } + return "", false +} + // countWord is a count with its noun: one metric, three metrics, no judges. // A zero is said rather than printed bare, for the reason every other // nothing here is said. diff --git a/cmd/codeaf/pool_test.go b/cmd/codeaf/pool_test.go index f4131163e..d212370d2 100644 --- a/cmd/codeaf/pool_test.go +++ b/cmd/codeaf/pool_test.go @@ -983,12 +983,12 @@ func TestPoolShowPrintsEachMetricAfterTheIndexLine(t *testing.T) { } body := out.String() indexAt := strings.Index(body, "index · ") - qualityAt := strings.Index(body, "role_quality: gaussian score · 1 cell · dims role, model") sharesAt := strings.Index(body, "acceptable: bernoulli share · 3 cells · dims role, model, source · sources grader, reviewer") - if indexAt < 0 || qualityAt < 0 || sharesAt < 0 { + qualityAt := strings.Index(body, "role_quality: gaussian score · 1 cell · dims role, model") + if indexAt < 0 || sharesAt < 0 || qualityAt < 0 { t.Fatalf("show did not print both metrics beside the index line:\n%s", body) } - if qualityAt < indexAt || sharesAt < qualityAt { + if sharesAt < indexAt || qualityAt < sharesAt { t.Fatalf("the metric lines did not follow the index line in the index's own order:\n%s", body) } } diff --git a/internal/pool/index/index.go b/internal/pool/index/index.go index 7367088a1..4cb7aa9ab 100644 --- a/internal/pool/index/index.go +++ b/internal/pool/index/index.go @@ -60,10 +60,11 @@ type Index struct { minInstalls int judges []string rubrics map[string]int - // metrics holds the declared names as spelled; kinds and dims are keyed by - // the folded name every lookup arrives under. + // metrics holds the declared names as spelled; kinds, units and dims are + // keyed by the folded name every lookup arrives under. metrics map[string]string kinds map[string]string + units map[string]string dims map[string]map[string]bool // aliases maps a folded, ~-stripped id to the canonical id as the document // spells it. The canonical id is in the map under its own folded form. @@ -124,6 +125,7 @@ type document struct { type metricDecl struct { Kind string `json:"kind"` + Unit string `json:"unit"` Dims []string `json:"dims"` } @@ -167,6 +169,7 @@ func parse(data []byte) (*Index, error) { rubrics: d.Rubrics, metrics: map[string]string{}, kinds: map[string]string{}, + units: map[string]string{}, dims: map[string]map[string]bool{}, aliases: map[string]string{}, cells: map[string]map[string]Cell{}, @@ -197,6 +200,9 @@ func parse(data []byte) (*Index, error) { if _, seen := x.kinds[folded]; !seen { x.kinds[folded] = fold(decl.Kind) } + if _, seen := x.units[folded]; !seen { + x.units[folded] = fold(decl.Unit) + } dimset := map[string]bool{} for _, dim := range decl.Dims { fd := fold(dim) @@ -396,6 +402,27 @@ func (x *Index) Kind(metric string) (string, bool) { return kind, ok } +// Unit answers with the unit word the document spells for the metric, +// lowercased, whether or not this build knows it. A metric that spells no +// unit, and a name the document does not declare, answer empty. +func (x *Index) Unit(metric string) string { + return x.units[fold(metric)] +} + +// Dims answers with the metric's declared dims beyond role and model, +// sorted. Role and model are the address of every cell and are never +// repeated here. A metric that declares no dim beyond them, and a name the +// document does not declare, answer empty. +func (x *Index) Dims(metric string) []string { + set := x.dims[fold(metric)] + out := make([]string, 0, len(set)) + for dim := range set { + out = append(out, dim) + } + sort.Strings(out) + return out +} + // Canonical answers with the canonical id the document spells for a model id, // for the id itself and for any of its alternates, and with the normalised // input for an id the document never names. From f1e8bad4e460d7a20599b2888f66679027d8545d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 22:53:07 -0400 Subject: [PATCH 3/4] pool: a change note for show and verify printing both metrics Co-Authored-By: codeaf --- ...0-pool-show-and-verify-print-both-metrics.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/changes/unreleased/0000-pool-show-and-verify-print-both-metrics.md diff --git a/docs/changes/unreleased/0000-pool-show-and-verify-print-both-metrics.md b/docs/changes/unreleased/0000-pool-show-and-verify-print-both-metrics.md new file mode 100644 index 000000000..2ca972662 --- /dev/null +++ b/docs/changes/unreleased/0000-pool-show-and-verify-print-both-metrics.md @@ -0,0 +1,17 @@ +--- +kind: changed +title: pool show and verify say both metrics the index carries +pr: 0000 +surface: [engine] +invalidates: + - "`codeaf pool show` printed the held index as a metric count (`… 2 metrics …`) and `codeaf pool verify` ended its sentence with the same count, so the relay's second metric was invisible and a person could not tell which cells are judged scores and which are graded shares. show now prints one line per declared metric after the index line — `role_quality: gaussian score · 12 cells · dims role, model`, and for the metric whose cells are split by the grader or judge that produced each share, the distinct sources beside them: `acceptable: bernoulli share · 7 cells · dims role, model, source · sources reviewer, grader`. verify says `metrics role_quality, acceptable` where it counted." + - "The `--json` answers of show, status and verify carry `metric_list` — one object per declared metric with `name`, `kind`, `unit`, `dims`, `cells` and, where the cells are split by one, `sources` — beside the `metrics` count, which stays an integer: the count is what a reader of today's shape already reads, and the array is the detail added beside it, not instead of it." + - "`internal/pool/index` read a metric's `unit` from the document and dropped it unread. `Unit(metric)` and `Dims(metric)` now answer the words the document spells — the unit folded like `Kind`'s word, the dims the metric declares beyond role and model, sorted — and a one-metric document, the seed's, prints one line." +--- + +The line shape follows the index's own order, sorted, and a metric the +document spells no unit for says its kind alone. The sources are gathered +off the cells, so a metric without a `source` dim says none, and a source +the cells spell two ways is matched the way the index matches a name — +lowercased, trimmed — and said once. `verify --json` carries the same array +beside its count, so both doors answer the same shape. \ No newline at end of file From 205face0abfbed8d49c1cdb19d162dc271b9bb0b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 22:57:29 -0400 Subject: [PATCH 4/4] changes: stamp the metrics note with its pull request Co-Authored-By: Claude Fable 5.1 --- ...ics.md => 1141-pool-show-and-verify-print-both-metrics.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename docs/changes/unreleased/{0000-pool-show-and-verify-print-both-metrics.md => 1141-pool-show-and-verify-print-both-metrics.md} (96%) diff --git a/docs/changes/unreleased/0000-pool-show-and-verify-print-both-metrics.md b/docs/changes/unreleased/1141-pool-show-and-verify-print-both-metrics.md similarity index 96% rename from docs/changes/unreleased/0000-pool-show-and-verify-print-both-metrics.md rename to docs/changes/unreleased/1141-pool-show-and-verify-print-both-metrics.md index 2ca972662..e6cd730bb 100644 --- a/docs/changes/unreleased/0000-pool-show-and-verify-print-both-metrics.md +++ b/docs/changes/unreleased/1141-pool-show-and-verify-print-both-metrics.md @@ -1,7 +1,7 @@ --- kind: changed title: pool show and verify say both metrics the index carries -pr: 0000 +pr: 1141 surface: [engine] invalidates: - "`codeaf pool show` printed the held index as a metric count (`… 2 metrics …`) and `codeaf pool verify` ended its sentence with the same count, so the relay's second metric was invisible and a person could not tell which cells are judged scores and which are graded shares. show now prints one line per declared metric after the index line — `role_quality: gaussian score · 12 cells · dims role, model`, and for the metric whose cells are split by the grader or judge that produced each share, the distinct sources beside them: `acceptable: bernoulli share · 7 cells · dims role, model, source · sources reviewer, grader`. verify says `metrics role_quality, acceptable` where it counted." @@ -14,4 +14,4 @@ document spells no unit for says its kind alone. The sources are gathered off the cells, so a metric without a `source` dim says none, and a source the cells spell two ways is matched the way the index matches a name — lowercased, trimmed — and said once. `verify --json` carries the same array -beside its count, so both doors answer the same shape. \ No newline at end of file +beside its count, so both doors answer the same shape.