retrieval-eval 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +342 -0
  3. package/dist/cli.js +1344 -0
  4. package/dist/index.d.ts +376 -0
  5. package/dist/index.js +971 -0
  6. package/package.json +74 -0
  7. package/spec/README.md +31 -0
  8. package/spec/chunk-id.md +51 -0
  9. package/spec/drift.md +49 -0
  10. package/spec/fixtures/README.md +48 -0
  11. package/spec/fixtures/basic/expected.json +43 -0
  12. package/spec/fixtures/basic/judgments.jsonl +4 -0
  13. package/spec/fixtures/basic/run.jsonl +2 -0
  14. package/spec/fixtures/chunk-id/expected.json +32 -0
  15. package/spec/fixtures/drift/corpus.json +38 -0
  16. package/spec/fixtures/drift/expected.json +22 -0
  17. package/spec/fixtures/drift/judgments.jsonl +4 -0
  18. package/spec/fixtures/duplicate-ranking/expected.json +56 -0
  19. package/spec/fixtures/duplicate-ranking/judgments.jsonl +3 -0
  20. package/spec/fixtures/duplicate-ranking/run-duplicate-query.jsonl +2 -0
  21. package/spec/fixtures/duplicate-ranking/run-non-string-key.jsonl +1 -0
  22. package/spec/fixtures/duplicate-ranking/run.jsonl +2 -0
  23. package/spec/fixtures/merge/corpus.json +22 -0
  24. package/spec/fixtures/merge/expected.json +21 -0
  25. package/spec/fixtures/merge/judgments.jsonl +3 -0
  26. package/spec/fixtures/no-positives/expected.json +17 -0
  27. package/spec/fixtures/no-positives/judgments.jsonl +4 -0
  28. package/spec/fixtures/no-positives/run.jsonl +2 -0
  29. package/spec/fixtures/nothing-scored/expected.json +9 -0
  30. package/spec/fixtures/nothing-scored/judgments.jsonl +1 -0
  31. package/spec/fixtures/nothing-scored/run.jsonl +1 -0
  32. package/spec/fixtures/qrels/beir.tsv +4 -0
  33. package/spec/fixtures/qrels/expected.json +14 -0
  34. package/spec/fixtures/qrels/trec.qrels +4 -0
  35. package/spec/fixtures/strata/expected.json +40 -0
  36. package/spec/fixtures/strata/judgments.jsonl +5 -0
  37. package/spec/fixtures/strata/run.jsonl +5 -0
  38. package/spec/fixtures/stratum-order/expected.json +30 -0
  39. package/spec/fixtures/stratum-order/judgments.jsonl +5 -0
  40. package/spec/fixtures/stratum-order/run.jsonl +5 -0
  41. package/spec/fixtures/summarize/expected.json +53 -0
  42. package/spec/fixtures/unsorted-queries/expected.json +23 -0
  43. package/spec/fixtures/unsorted-queries/judgments.jsonl +3 -0
  44. package/spec/fixtures/unsound-judgments/expected.json +19 -0
  45. package/spec/fixtures/unsound-judgments/judgments.jsonl +3 -0
  46. package/spec/fixtures/unsound-judgments/run.jsonl +1 -0
  47. package/spec/judgments.schema.json +54 -0
  48. package/spec/report.schema.json +227 -0
@@ -0,0 +1,40 @@
1
+ {
2
+ "k": 3,
3
+ "metrics": {
4
+ "precision@3": 0.2,
5
+ "recall@3": 0.6,
6
+ "ndcg@3": 0.6,
7
+ "mrr@3": 0.6,
8
+ "map@3": 0.6,
9
+ "hit_rate@3": 0.6
10
+ },
11
+ "per_stratum": {
12
+ "billing": {
13
+ "n": 3,
14
+ "metrics": {
15
+ "precision@3": 0.333333333333,
16
+ "recall@3": 1.0,
17
+ "ndcg@3": 1.0,
18
+ "mrr@3": 1.0,
19
+ "map@3": 1.0,
20
+ "hit_rate@3": 1.0
21
+ }
22
+ },
23
+ "legal": {
24
+ "n": 2,
25
+ "metrics": {
26
+ "precision@3": 0.0,
27
+ "recall@3": 0.0,
28
+ "ndcg@3": 0.0,
29
+ "mrr@3": 0.0,
30
+ "map@3": 0.0,
31
+ "hit_rate@3": 0.0
32
+ }
33
+ }
34
+ },
35
+ "worst_stratum": {
36
+ "metric": "recall@3",
37
+ "name": "legal",
38
+ "value": 0.0
39
+ }
40
+ }
@@ -0,0 +1,5 @@
1
+ {"query_id": "b1", "query": "billing 1", "doc_uri": "bd1", "relevance": 1, "stratum": "billing", "labeled_by": "human:fixture"}
2
+ {"query_id": "b2", "query": "billing 2", "doc_uri": "bd2", "relevance": 1, "stratum": "billing", "labeled_by": "human:fixture"}
3
+ {"query_id": "b3", "query": "billing 3", "doc_uri": "bd3", "relevance": 1, "stratum": "billing", "labeled_by": "human:fixture"}
4
+ {"query_id": "l1", "query": "legal 1", "doc_uri": "ld1", "relevance": 1, "stratum": "legal", "labeled_by": "human:fixture"}
5
+ {"query_id": "l2", "query": "legal 2", "doc_uri": "ld2", "relevance": 1, "stratum": "legal", "labeled_by": "human:fixture"}
@@ -0,0 +1,5 @@
1
+ {"query_id": "b1", "ranking": ["bd1", "x", "y"]}
2
+ {"query_id": "b2", "ranking": ["bd2", "x", "y"]}
3
+ {"query_id": "b3", "ranking": ["bd3", "x", "y"]}
4
+ {"query_id": "l1", "ranking": ["x", "y", "z"]}
5
+ {"query_id": "l2", "ranking": ["x", "y", "z"]}
@@ -0,0 +1,30 @@
1
+ {
2
+ "k": 3,
3
+ "threshold": 2,
4
+ "note": "Five strata chosen so that code-point order and locale collation disagree. Scored at threshold 2, Beta/alpha/m all reach recall 1.0 and tie, so the table falls back to the name; zeta and etude have nothing at the threshold and are listed after, unscored. JavaScript's localeCompare orders these by locale, which disagrees with Python and reorders them again on a machine with a different locale. Both implementations sort by Unicode code point, which is locale-independent and is what Python's sorted does.",
5
+ "scored_order": [
6
+ "Beta",
7
+ "alpha",
8
+ "m"
9
+ ],
10
+ "unscored_order": [
11
+ "zeta",
12
+ "étude"
13
+ ],
14
+ "per_stratum_n": {
15
+ "Beta": 1,
16
+ "alpha": 1,
17
+ "m": 1,
18
+ "zeta": 0,
19
+ "étude": 0
20
+ },
21
+ "metrics": {
22
+ "recall@3": 1.0,
23
+ "precision@3": 0.333333333333,
24
+ "ndcg@3": 1.0,
25
+ "mrr@3": 1.0,
26
+ "map@3": 1.0,
27
+ "hit_rate@3": 1.0
28
+ },
29
+ "locale_order_is_not_asserted": "Collation order varies by machine, which is the objection to it; only the code-point orders above are normative."
30
+ }
@@ -0,0 +1,5 @@
1
+ {"query_id": "q1", "query": "beta query", "doc_uri": "d1", "relevance": 2, "labeled_by": "human:fixture", "stratum": "Beta"}
2
+ {"query_id": "q2", "query": "alpha query", "doc_uri": "d2", "relevance": 2, "labeled_by": "human:fixture", "stratum": "alpha"}
3
+ {"query_id": "q3", "query": "m query", "doc_uri": "d3", "relevance": 2, "labeled_by": "human:fixture", "stratum": "m"}
4
+ {"query_id": "q4", "query": "zeta query", "doc_uri": "d4", "relevance": 1, "labeled_by": "human:fixture", "stratum": "zeta"}
5
+ {"query_id": "q5", "query": "etude query", "doc_uri": "d5", "relevance": 1, "labeled_by": "human:fixture", "stratum": "étude"}
@@ -0,0 +1,5 @@
1
+ {"query_id": "q1", "ranking": ["d1"]}
2
+ {"query_id": "q2", "ranking": ["d2"]}
3
+ {"query_id": "q3", "ranking": ["d3"]}
4
+ {"query_id": "q4", "ranking": ["d4"]}
5
+ {"query_id": "q5", "ranking": ["d5"]}
@@ -0,0 +1,53 @@
1
+ {
2
+ "note": "95% Student-t interval on the sample mean, with the sample standard deviation (n-1). A single sample yields no interval at all, because one draw from a non-deterministic instrument is not evidence of a spread. The interval is not clamped: raising a negative lower bound to 0 would let a ci-lower gate pass.",
3
+ "cases": {
4
+ "five_samples_of_one_triple": {
5
+ "samples": [0.8, 1.0, 0.6, 0.9, 0.7],
6
+ "expected": {
7
+ "value": 0.8,
8
+ "n": 5,
9
+ "stdev": 0.158113883008,
10
+ "ci": [0.603675683852, 0.996324316148],
11
+ "deterministic": false
12
+ }
13
+ },
14
+ "single_sample_has_no_interval": {
15
+ "samples": [0.82],
16
+ "expected": {
17
+ "value": 0.82,
18
+ "n": 1,
19
+ "deterministic": false
20
+ }
21
+ },
22
+ "identical_samples_have_a_zero_width_interval": {
23
+ "samples": [0.9, 0.9, 0.9],
24
+ "expected": {
25
+ "value": 0.9,
26
+ "n": 3,
27
+ "stdev": 0.0,
28
+ "ci": [0.9, 0.9],
29
+ "deterministic": false
30
+ }
31
+ },
32
+ "interval_is_not_clamped_to_the_metric_range": {
33
+ "samples": [0.95, 1.0, 1.0, 0.99],
34
+ "expected": {
35
+ "value": 0.985,
36
+ "n": 4,
37
+ "stdev": 0.023804761428,
38
+ "ci": [0.947121312472, 1.022878687528],
39
+ "deterministic": false
40
+ }
41
+ },
42
+ "lower_bound_may_fall_below_zero": {
43
+ "samples": [0.0, 0.0, 1.0],
44
+ "expected": {
45
+ "value": 0.333333333333,
46
+ "n": 3,
47
+ "stdev": 0.57735026919,
48
+ "ci": [-1.10088424325, 1.767550909916],
49
+ "deterministic": false
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "note": "The query ids arrive unsorted and none carries query text, so every one raises `missing-query-text`. An ordering that reaches `--json` and the rendered report is part of what the two implementations must agree on, so both emit these sorted by Unicode code point rather than in file order. The strata are unsorted too, for the same reason: `thin-stratum` follows sorted order, not insertion order.",
3
+ "issue_order": [
4
+ "missing-query-text:q1",
5
+ "missing-query-text:q2",
6
+ "missing-query-text:q3",
7
+ "thin-stratum:alpha",
8
+ "thin-stratum:zeta",
9
+ "no-chunk-ids"
10
+ ],
11
+ "missing_query_text_order": [
12
+ "q1",
13
+ "q2",
14
+ "q3"
15
+ ],
16
+ "thin_stratum_order": [
17
+ "alpha",
18
+ "zeta"
19
+ ],
20
+ "ok": true,
21
+ "queries": 3,
22
+ "labels": 3
23
+ }
@@ -0,0 +1,3 @@
1
+ {"query_id": "q3", "doc_uri": "d0", "relevance": 1, "labeled_by": "human:fixture"}
2
+ {"query_id": "q1", "doc_uri": "d1", "relevance": 1, "labeled_by": "human:fixture", "stratum": "zeta"}
3
+ {"query_id": "q2", "doc_uri": "d2", "relevance": 1, "labeled_by": "human:fixture", "stratum": "alpha"}
@@ -0,0 +1,19 @@
1
+ {
2
+ "k": 2,
3
+ "note": "Alice and Bob disagree about d1. `validate` calls that an error and exits non-zero, so `score` cannot look at the same file, resolve the conflict last-wins and report PASS. The verdict is INDETERMINATE: there is a number, but the ground truth it was computed from contradicts itself, and a gate that passes over it has proved nothing.",
4
+ "validate": {
5
+ "ok": false,
6
+ "error_codes": ["duplicate-label"]
7
+ },
8
+ "queries": 1,
9
+ "queries_scored": 1,
10
+ "verdict": "INDETERMINATE",
11
+ "reason": "the judgment set is unsound (duplicate-label); run 'retrieval-eval validate' for detail. No metric computed from it can be trusted",
12
+ "gate": {
13
+ "expression": "recall@2:0.1",
14
+ "gate_status": "PASS",
15
+ "note": "The gate itself passes. The verdict does not, because a gate cannot clear a finding it never looked at.",
16
+ "verdict": "INDETERMINATE",
17
+ "exit_code": 1
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ {"query_id": "q1", "query": "how do I reset my password", "doc_uri": "d1", "relevance": 2, "labeled_by": "human:alice"}
2
+ {"query_id": "q1", "query": "how do I reset my password", "doc_uri": "d1", "relevance": 0, "labeled_by": "human:bob"}
3
+ {"query_id": "q1", "query": "how do I reset my password", "doc_uri": "d2", "relevance": 1, "labeled_by": "human:alice"}
@@ -0,0 +1 @@
1
+ {"query_id": "q1", "ranking": ["d1", "d2"]}
@@ -0,0 +1,54 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://retrieval-eval.dev/spec/judgments.schema.json",
4
+ "title": "retrieval-eval judgment",
5
+ "description": "One relevance judgment. A judgments file is JSONL: one of these per line.",
6
+ "type": "object",
7
+ "required": ["query_id", "doc_uri", "relevance"],
8
+ "properties": {
9
+ "query_id": { "type": "string", "minLength": 1, "description": "Stable id for the query." },
10
+ "query": {
11
+ "type": "string",
12
+ "description": "Query text. Required on at least one row per query_id."
13
+ },
14
+ "doc_uri": {
15
+ "type": "string",
16
+ "minLength": 1,
17
+ "description": "Canonical source document uri."
18
+ },
19
+ "chunk_id": {
20
+ "type": "string",
21
+ "pattern": "^c1:[0-9a-f]{32}$",
22
+ "description": "Content-addressed chunk id. Omit for document-level judgments."
23
+ },
24
+ "text_sha": {
25
+ "type": "string",
26
+ "pattern": "^t1:[0-9a-f]{32}$",
27
+ "description": "Hash of the normalized labeled text. Enables drift re-anchoring without storing text."
28
+ },
29
+ "chunk_text": {
30
+ "type": "string",
31
+ "description": "The labeled text. Enables SPLIT detection. Omit for sensitive corpora."
32
+ },
33
+ "relevance": {
34
+ "type": "integer",
35
+ "minimum": 0,
36
+ "description": "Graded relevance. 0 = not relevant. TREC convention: 0-3."
37
+ },
38
+ "corpus_fingerprint": {
39
+ "type": "string",
40
+ "description": "State of corpus+chunker+embedder this judgment was made against."
41
+ },
42
+ "labeled_by": {
43
+ "type": "string",
44
+ "description": "Provenance, e.g. 'human:ana' or 'synthetic:claude-haiku-4-5'. Prefix with human: or synthetic:."
45
+ },
46
+ "labeled_at": { "type": "string", "format": "date-time" },
47
+ "stratum": {
48
+ "type": "string",
49
+ "description": "Query category, for coverage reporting instead of averages."
50
+ },
51
+ "notes": { "type": "string" }
52
+ },
53
+ "additionalProperties": true
54
+ }
@@ -0,0 +1,227 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://retrieval-eval.dev/spec/report.schema.json",
4
+ "title": "retrieval-eval report",
5
+ "description": "Results any tool may emit and any CI or dashboard may ingest.",
6
+ "type": "object",
7
+ "required": [
8
+ "spec_version",
9
+ "tool",
10
+ "generated_at",
11
+ "corpus",
12
+ "judgments",
13
+ "metrics",
14
+ "verdict"
15
+ ],
16
+ "properties": {
17
+ "spec_version": {
18
+ "const": "1"
19
+ },
20
+ "tool": {
21
+ "type": "object",
22
+ "required": [
23
+ "name",
24
+ "version"
25
+ ],
26
+ "properties": {
27
+ "name": {
28
+ "type": "string"
29
+ },
30
+ "version": {
31
+ "type": "string"
32
+ }
33
+ }
34
+ },
35
+ "generated_at": {
36
+ "type": "string",
37
+ "format": "date-time"
38
+ },
39
+ "corpus": {
40
+ "type": "object",
41
+ "properties": {
42
+ "fingerprint": {
43
+ "type": [
44
+ "string",
45
+ "null"
46
+ ]
47
+ },
48
+ "documents": {
49
+ "type": "integer"
50
+ },
51
+ "chunks": {
52
+ "type": "integer"
53
+ }
54
+ }
55
+ },
56
+ "judgments": {
57
+ "type": "object",
58
+ "required": [
59
+ "queries",
60
+ "labels"
61
+ ],
62
+ "properties": {
63
+ "queries": {
64
+ "type": "integer"
65
+ },
66
+ "queries_scored": {
67
+ "type": "integer",
68
+ "minimum": 0,
69
+ "description": "Queries the averages were computed from: those with at least one label at or above the relevance threshold. Queries with nothing relevant to find are excluded rather than scored zero."
70
+ },
71
+ "labels": {
72
+ "type": "integer"
73
+ },
74
+ "fingerprint": {
75
+ "type": [
76
+ "string",
77
+ "null"
78
+ ]
79
+ },
80
+ "human_labels": {
81
+ "type": "integer"
82
+ },
83
+ "synthetic_labels": {
84
+ "type": "integer"
85
+ },
86
+ "drift": {
87
+ "description": "Present when judgment validity was checked against a live corpus.",
88
+ "type": "object",
89
+ "properties": {
90
+ "valid": {
91
+ "type": "integer"
92
+ },
93
+ "re_anchorable": {
94
+ "type": "integer"
95
+ },
96
+ "merged": {
97
+ "type": "integer"
98
+ },
99
+ "split": {
100
+ "type": "integer"
101
+ },
102
+ "orphaned": {
103
+ "type": "integer"
104
+ },
105
+ "invalid_ratio": {
106
+ "type": "number",
107
+ "minimum": 0,
108
+ "maximum": 1
109
+ }
110
+ }
111
+ }
112
+ }
113
+ },
114
+ "metrics": {
115
+ "description": "Metric name to measurement. Every cutoff metric carries its cutoff in the name: precision@k, recall@k, ndcg@k, mrr@k, map@k, hit_rate@k.",
116
+ "type": "object",
117
+ "additionalProperties": {
118
+ "$ref": "#/$defs/measurement"
119
+ }
120
+ },
121
+ "per_stratum": {
122
+ "description": "MANDATORY when judgments carry strata. Averages hide broken query classes.",
123
+ "type": "object",
124
+ "additionalProperties": {
125
+ "type": "object",
126
+ "required": [
127
+ "n"
128
+ ],
129
+ "properties": {
130
+ "n": {
131
+ "type": "integer"
132
+ },
133
+ "metrics": {
134
+ "type": "object",
135
+ "additionalProperties": {
136
+ "$ref": "#/$defs/measurement"
137
+ }
138
+ }
139
+ }
140
+ }
141
+ },
142
+ "verdict": {
143
+ "type": "object",
144
+ "required": [
145
+ "status"
146
+ ],
147
+ "properties": {
148
+ "status": {
149
+ "enum": [
150
+ "PASS",
151
+ "FAIL",
152
+ "INDETERMINATE"
153
+ ]
154
+ },
155
+ "reasons": {
156
+ "type": "array",
157
+ "items": {
158
+ "type": "string"
159
+ }
160
+ },
161
+ "gates": {
162
+ "type": "array",
163
+ "items": {
164
+ "type": "object",
165
+ "required": [
166
+ "expression",
167
+ "status"
168
+ ],
169
+ "properties": {
170
+ "expression": {
171
+ "type": "string"
172
+ },
173
+ "status": {
174
+ "enum": [
175
+ "PASS",
176
+ "FAIL",
177
+ "INDETERMINATE"
178
+ ]
179
+ },
180
+ "observed": {
181
+ "type": "number"
182
+ },
183
+ "baseline": {
184
+ "type": [
185
+ "number",
186
+ "null"
187
+ ]
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+ },
195
+ "$defs": {
196
+ "measurement": {
197
+ "type": "object",
198
+ "required": [
199
+ "value"
200
+ ],
201
+ "properties": {
202
+ "value": {
203
+ "type": "number"
204
+ },
205
+ "n": {
206
+ "type": "integer",
207
+ "description": "Samples. >1 for non-deterministic (LLM-judged) metrics."
208
+ },
209
+ "stdev": {
210
+ "type": "number"
211
+ },
212
+ "ci": {
213
+ "description": "REQUIRED for any LLM-judged metric. A point estimate from one sample is a measurement lie.",
214
+ "type": "array",
215
+ "items": {
216
+ "type": "number"
217
+ },
218
+ "minItems": 2,
219
+ "maxItems": 2
220
+ },
221
+ "deterministic": {
222
+ "type": "boolean"
223
+ }
224
+ }
225
+ }
226
+ }
227
+ }