cu-cli-core 0.1.0b1__py3-none-any.whl

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.
cu_cli_core/errors.py ADDED
@@ -0,0 +1,77 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Frontend-neutral CU error types."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Any, Mapping
11
+
12
+
13
+ class ErrorCategory(str, Enum):
14
+ USAGE = "usage"
15
+ VALIDATION = "validation"
16
+ AUTHENTICATION = "authentication"
17
+ NOT_FOUND = "not-found"
18
+ CONFLICT = "conflict"
19
+ SERVICE = "service"
20
+ LOCAL_IO = "local-io"
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class ServiceErrorDetail:
25
+ code: str | None = None
26
+ message: str | None = None
27
+ target: str | None = None
28
+
29
+
30
+ class CuCoreError(Exception):
31
+ """Structured failure translated by each command frontend."""
32
+
33
+ category = ErrorCategory.SERVICE
34
+
35
+ def __init__(
36
+ self,
37
+ message: str,
38
+ *,
39
+ hint: str | None = None,
40
+ status_code: int | None = None,
41
+ details: tuple[ServiceErrorDetail, ...] = (),
42
+ context: Mapping[str, Any] | None = None,
43
+ ) -> None:
44
+ super().__init__(message)
45
+ self.message = message
46
+ self.hint = hint
47
+ self.status_code = status_code
48
+ self.details = details
49
+ self.context = dict(context or {})
50
+
51
+
52
+ class UsageError(CuCoreError):
53
+ category = ErrorCategory.USAGE
54
+
55
+
56
+ class ValidationError(CuCoreError):
57
+ category = ErrorCategory.VALIDATION
58
+
59
+
60
+ class AuthenticationError(CuCoreError):
61
+ category = ErrorCategory.AUTHENTICATION
62
+
63
+
64
+ class NotFoundError(CuCoreError):
65
+ category = ErrorCategory.NOT_FOUND
66
+
67
+
68
+ class ConflictError(CuCoreError):
69
+ category = ErrorCategory.CONFLICT
70
+
71
+
72
+ class ServiceError(CuCoreError):
73
+ category = ErrorCategory.SERVICE
74
+
75
+
76
+ class LocalIOError(CuCoreError):
77
+ category = ErrorCategory.LOCAL_IO
@@ -0,0 +1,322 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections import Counter
7
+ from fnmatch import fnmatch
8
+ import hashlib
9
+ import os
10
+ from pathlib import Path
11
+ from typing import Iterable, Sequence
12
+
13
+ from .contracts import (
14
+ ExecutionPlan,
15
+ ExistingResultPolicy,
16
+ InputOrigin,
17
+ InputPlan,
18
+ PlannedInput,
19
+ PlannedOutput,
20
+ ResultView,
21
+ SelectionMode,
22
+ SkippedInput,
23
+ )
24
+ from .errors import UsageError, ValidationError
25
+
26
+ _WILDCARD_CHARS = frozenset("*?[")
27
+ _RESULT_SUFFIX = {
28
+ ResultView.LLM_INPUT: ".result.md",
29
+ ResultView.FULL: ".result.json",
30
+ }
31
+ _GENERATED_RESULT_SUFFIXES = tuple(_RESULT_SUFFIX.values())
32
+
33
+
34
+ def _reject_direct_duplicates(values: Sequence[str | Path], option: str) -> None:
35
+ rendered = [os.fspath(value) for value in values]
36
+ duplicates = sorted(value for value, count in Counter(rendered).items() if count > 1)
37
+ if duplicates:
38
+ joined = ", ".join(duplicates)
39
+ raise UsageError(f"{option} was provided more than once for: {joined}")
40
+
41
+
42
+ def _file_identity(path: Path) -> object:
43
+ stat = path.stat()
44
+ if stat.st_ino:
45
+ return (stat.st_dev, stat.st_ino)
46
+ return os.path.normcase(os.fspath(path.resolve()))
47
+
48
+
49
+ def _validated_file(path: Path, *, option: str) -> tuple[Path, int]:
50
+ if not path.exists():
51
+ raise ValidationError(f"{option} does not exist: {path}")
52
+ if not path.is_file():
53
+ raise ValidationError(f"{option} must identify a file: {path}")
54
+ try:
55
+ stat = path.stat()
56
+ except OSError as exc:
57
+ raise ValidationError(f"{option} cannot be read: {path}") from exc
58
+ return path.resolve(), stat.st_size
59
+
60
+
61
+ def _validated_source(path: Path, *, option: str) -> Path:
62
+ if not path.exists():
63
+ raise ValidationError(f"{option} does not exist: {path}")
64
+ if not path.is_dir():
65
+ raise ValidationError(f"{option} must identify a directory: {path}")
66
+ return path.resolve()
67
+
68
+
69
+ def _directory_files(
70
+ source: Path,
71
+ *,
72
+ recursive: bool,
73
+ pattern: str,
74
+ skipped: dict[Path, SkippedInput],
75
+ ) -> Iterable[Path]:
76
+ candidates = source.rglob("*") if recursive else source.iterdir()
77
+ files = (path for path in candidates if path.is_file())
78
+ selected: list[Path] = []
79
+ for path in files:
80
+ relative_path = path.relative_to(source)
81
+ if not fnmatch(relative_path.as_posix(), pattern):
82
+ continue
83
+ if path.name.endswith(_GENERATED_RESULT_SUFFIXES):
84
+ continue
85
+ if any(part.startswith(".") for part in relative_path.parts[:-1]):
86
+ continue
87
+ if path.name.startswith("."):
88
+ skipped.setdefault(
89
+ path,
90
+ SkippedInput(path=path, reason="hidden file skipped"),
91
+ )
92
+ continue
93
+ selected.append(path)
94
+ return sorted(selected, key=lambda path: path.relative_to(source).as_posix())
95
+
96
+
97
+ def plan_inputs(
98
+ *,
99
+ positional: Sequence[str | Path] = (),
100
+ files: Sequence[str | Path] = (),
101
+ sources: Sequence[str | Path] = (),
102
+ pattern: str | None = None,
103
+ recursive: bool = False,
104
+ ) -> InputPlan:
105
+ """Validate and expand one invocation's local input selection."""
106
+ if positional and (files or sources):
107
+ conflicts = []
108
+ if files:
109
+ conflicts.append("--file")
110
+ if sources:
111
+ conflicts.append("--source")
112
+ raise UsageError(
113
+ "positional inputs cannot be combined with " + " or ".join(conflicts) + "."
114
+ )
115
+ if files and sources:
116
+ raise UsageError("--file and --source cannot be combined.")
117
+ if pattern is not None and not sources:
118
+ raise UsageError("--pattern is valid only with --source.")
119
+ if not positional and not files and not sources:
120
+ raise UsageError("provide positional inputs, --file, or --source.")
121
+
122
+ if positional:
123
+ mode = SelectionMode.POSITIONAL
124
+ direct = positional
125
+ option = "positional input"
126
+ elif files:
127
+ mode = SelectionMode.NAMED_FILES
128
+ direct = files
129
+ option = "--file"
130
+ else:
131
+ mode = SelectionMode.NAMED_SOURCES
132
+ direct = sources
133
+ option = "--source"
134
+ _reject_direct_duplicates(direct, option)
135
+
136
+ selected: list[PlannedInput] = []
137
+ seen: set[object] = set()
138
+ skipped: dict[Path, SkippedInput] = {}
139
+ includes_directory = bool(sources)
140
+
141
+ def add_file(
142
+ path: Path,
143
+ *,
144
+ source_root: Path,
145
+ relative_path: Path,
146
+ origin: InputOrigin,
147
+ ) -> None:
148
+ resolved, size = _validated_file(path, option=option)
149
+ identity = _file_identity(resolved)
150
+ if identity in seen:
151
+ return
152
+ seen.add(identity)
153
+ selected.append(
154
+ PlannedInput(
155
+ path=resolved,
156
+ source_root=source_root,
157
+ relative_path=relative_path,
158
+ origin=origin,
159
+ size_bytes=size,
160
+ )
161
+ )
162
+
163
+ if positional:
164
+ for value in positional:
165
+ text = os.fspath(value)
166
+ if any(char in text for char in _WILDCARD_CHARS):
167
+ raise UsageError(
168
+ f"wildcard patterns aren't accepted as positional inputs: {text}",
169
+ hint='Use --source with --pattern, for example: --source . --pattern "*.pdf"',
170
+ )
171
+ path = Path(value)
172
+ if not path.exists():
173
+ raise ValidationError(f"positional input does not exist: {path}")
174
+ if path.is_dir():
175
+ includes_directory = True
176
+ source = _validated_source(path, option=option)
177
+ for child in _directory_files(
178
+ source,
179
+ recursive=recursive,
180
+ pattern="*",
181
+ skipped=skipped,
182
+ ):
183
+ add_file(
184
+ child,
185
+ source_root=source,
186
+ relative_path=child.relative_to(source),
187
+ origin=InputOrigin.POSITIONAL_SOURCE,
188
+ )
189
+ else:
190
+ resolved, _ = _validated_file(path, option=option)
191
+ add_file(
192
+ resolved,
193
+ source_root=resolved.parent,
194
+ relative_path=Path(resolved.name),
195
+ origin=InputOrigin.POSITIONAL_FILE,
196
+ )
197
+ elif files:
198
+ for value in files:
199
+ path, _ = _validated_file(Path(value), option=option)
200
+ add_file(
201
+ path,
202
+ source_root=path.parent,
203
+ relative_path=Path(path.name),
204
+ origin=InputOrigin.NAMED_FILE,
205
+ )
206
+ else:
207
+ effective_pattern = pattern if pattern is not None else "*"
208
+ if not effective_pattern:
209
+ raise UsageError("--pattern cannot be empty.")
210
+ for value in sources:
211
+ source = _validated_source(Path(value), option=option)
212
+ for child in _directory_files(
213
+ source,
214
+ recursive=recursive,
215
+ pattern=effective_pattern,
216
+ skipped=skipped,
217
+ ):
218
+ add_file(
219
+ child,
220
+ source_root=source,
221
+ relative_path=child.relative_to(source),
222
+ origin=InputOrigin.NAMED_SOURCE,
223
+ )
224
+
225
+ if recursive and not includes_directory:
226
+ raise UsageError("--recursive is valid only when input selection includes a directory.")
227
+ if not selected:
228
+ if skipped:
229
+ details = ", ".join(
230
+ f"{item.path} ({item.reason})"
231
+ for item in skipped.values()
232
+ )
233
+ raise ValidationError(
234
+ "input selection did not find any analyzable files. "
235
+ f"Skipped during discovery: {details}."
236
+ )
237
+ raise ValidationError("input selection did not find any files.")
238
+
239
+ extension_counts = Counter(
240
+ item.path.suffix.lower() or "(none)" for item in selected
241
+ )
242
+ return InputPlan(
243
+ mode=mode,
244
+ inputs=tuple(selected),
245
+ recursive=recursive,
246
+ pattern=pattern,
247
+ total_bytes=sum(item.size_bytes for item in selected),
248
+ extension_counts=dict(sorted(extension_counts.items())),
249
+ skipped=tuple(
250
+ skipped[path]
251
+ for path in sorted(skipped, key=lambda path: os.fspath(path))
252
+ ),
253
+ )
254
+
255
+
256
+ def _result_path(path: Path, view: ResultView) -> Path:
257
+ return Path(f"{path}{_RESULT_SUFFIX[view]}")
258
+
259
+
260
+ def plan_outputs(
261
+ input_plan: InputPlan,
262
+ *,
263
+ view: ResultView,
264
+ output_file: str | Path | None = None,
265
+ output_dir: str | Path | None = None,
266
+ on_existing: ExistingResultPolicy = ExistingResultPolicy.ERROR,
267
+ stream_single: bool = True,
268
+ dry_run: bool = False,
269
+ ) -> ExecutionPlan:
270
+ """Resolve result destinations, collisions, and existing-file actions."""
271
+ if output_file is not None and output_dir is not None:
272
+ raise UsageError("--output-file and --output-dir cannot be combined.")
273
+ if output_file is not None and len(input_plan.inputs) != 1:
274
+ raise UsageError("--output-file is valid only when exactly one file is selected.")
275
+
276
+ destinations: list[Path | None] = []
277
+ for item in input_plan.inputs:
278
+ if output_file is not None:
279
+ destination = Path(output_file)
280
+ elif output_dir is not None:
281
+ relative = item.relative_path
282
+ if relative.is_absolute() or ".." in relative.parts:
283
+ raise ValidationError(
284
+ f"source-relative output path is invalid: {relative}"
285
+ )
286
+ destination = _result_path(Path(output_dir) / relative, view)
287
+ elif stream_single and len(input_plan.inputs) == 1:
288
+ destination = None
289
+ else:
290
+ destination = _result_path(item.path, view)
291
+ destinations.append(destination)
292
+
293
+ counts = Counter(path for path in destinations if path is not None)
294
+ collided = {path for path, count in counts.items() if count > 1}
295
+ for index, destination in enumerate(destinations):
296
+ if destination not in collided:
297
+ continue
298
+ digest = hashlib.sha1(
299
+ os.fspath(input_plan.inputs[index].path).encode("utf-8")
300
+ ).hexdigest()[:8]
301
+ suffix = _RESULT_SUFFIX[view]
302
+ assert destination is not None
303
+ base = destination.name[: -len(suffix)]
304
+ destinations[index] = destination.with_name(f"{base}.{digest}{suffix}")
305
+
306
+ outputs: list[PlannedOutput] = []
307
+ for item, destination in zip(input_plan.inputs, destinations, strict=True):
308
+ exists = destination is not None and destination.exists()
309
+ outputs.append(
310
+ PlannedOutput(
311
+ source=item,
312
+ path=destination,
313
+ exists=exists,
314
+ skipped=exists and on_existing is ExistingResultPolicy.SKIP,
315
+ )
316
+ )
317
+ return ExecutionPlan(
318
+ input_plan=input_plan,
319
+ outputs=tuple(outputs),
320
+ on_existing=on_existing,
321
+ dry_run=dry_run,
322
+ )
@@ -0,0 +1,4 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Client-injected Content Understanding operations."""
@@ -0,0 +1,294 @@
1
+ # Copyright (c) Microsoft Corporation.
2
+ # Licensed under the MIT license.
3
+
4
+ """Shared orchestration for analyze and analyzer-test commands."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Callable
9
+
10
+ from ..analysis import AnalyzeJob, AnalyzeOutcome, BatchResult, analyze_many
11
+ from ..contracts import AnalyzeRequest, AnalyzerTestRequest, InputPlan
12
+ from ..input_planning import plan_inputs
13
+
14
+ _TEST_DISCLAIMER = (
15
+ "This is not a real accuracy benchmark. It only checks whether a value was "
16
+ "extracted or generated, and reports confidence when the service returns it."
17
+ )
18
+
19
+
20
+ def _input_plan(request: AnalyzeRequest | AnalyzerTestRequest) -> InputPlan:
21
+ return plan_inputs(
22
+ positional=request.positional_inputs,
23
+ files=request.files,
24
+ sources=request.sources,
25
+ pattern=request.pattern,
26
+ recursive=request.recursive,
27
+ )
28
+
29
+
30
+ def execute_analyze(
31
+ client: Any,
32
+ request: AnalyzeRequest,
33
+ *,
34
+ input_plan: InputPlan | None = None,
35
+ jobs: list[AnalyzeJob] | None = None,
36
+ on_result: Callable[[AnalyzeOutcome], None] | None = None,
37
+ run: Callable[[Any, AnalyzeJob], Any] | None = None,
38
+ ) -> BatchResult:
39
+ """Execute a normalized analyze request against an injected client."""
40
+
41
+ planned = input_plan or _input_plan(request)
42
+ selected_jobs = jobs or [
43
+ AnalyzeJob(
44
+ input_ref=str(item.path),
45
+ analyzer_id=request.analyzer or "",
46
+ out_path=None,
47
+ )
48
+ for item in planned.inputs
49
+ ]
50
+ return analyze_many(
51
+ client,
52
+ selected_jobs,
53
+ concurrency=request.concurrency,
54
+ on_result=on_result,
55
+ run=run,
56
+ )
57
+
58
+
59
+ def _field_as_dict(value: Any) -> dict[str, Any] | None:
60
+ if isinstance(value, dict):
61
+ return value
62
+ if hasattr(value, "as_dict"):
63
+ mapped = value.as_dict()
64
+ return mapped if isinstance(mapped, dict) else None
65
+ return None
66
+
67
+
68
+ def _coerce_field_value(field: Any) -> Any:
69
+ if field is None or isinstance(field, (str, int, float, bool)):
70
+ return field
71
+ mapped = _field_as_dict(field)
72
+ if mapped is None:
73
+ return str(field)
74
+ for key in (
75
+ "valueString",
76
+ "valueNumber",
77
+ "valueInteger",
78
+ "valueBoolean",
79
+ "valueDate",
80
+ "valueTime",
81
+ "valueJson",
82
+ ):
83
+ if mapped.get(key) is not None:
84
+ return mapped[key]
85
+ if mapped.get("valueArray") is not None:
86
+ return [_coerce_field_value(value) for value in mapped["valueArray"]]
87
+ if isinstance(mapped.get("valueObject"), dict):
88
+ return {
89
+ key: _coerce_field_value(value)
90
+ for key, value in mapped["valueObject"].items()
91
+ }
92
+ for key in ("value", "content"):
93
+ if mapped.get(key) is not None:
94
+ return mapped[key]
95
+ return None
96
+
97
+
98
+ def _coerce_field_confidence(field: Any) -> float | None:
99
+ confidence = (
100
+ field.get("confidence")
101
+ if isinstance(field, dict)
102
+ else getattr(field, "confidence", None)
103
+ )
104
+ if confidence is None:
105
+ mapped = _field_as_dict(field)
106
+ confidence = mapped.get("confidence") if mapped else None
107
+ try:
108
+ return float(confidence) if confidence is not None else None
109
+ except (TypeError, ValueError):
110
+ return None
111
+
112
+
113
+ def _child_field_entries(
114
+ field_name: str,
115
+ field: Any,
116
+ ) -> dict[str, dict[str, Any]]:
117
+ mapped = _field_as_dict(field)
118
+ if mapped is None:
119
+ return {}
120
+ value_object = mapped.get("valueObject")
121
+ if isinstance(value_object, dict):
122
+ return {
123
+ f"{field_name}.{name}": {
124
+ "value": _coerce_field_value(value),
125
+ "confidence": _coerce_field_confidence(value),
126
+ }
127
+ for name, value in value_object.items()
128
+ }
129
+
130
+ value_array = mapped.get("valueArray")
131
+ if not isinstance(value_array, list):
132
+ return {}
133
+ child_values: dict[str, list[Any]] = {}
134
+ child_confidences: dict[str, list[float]] = {}
135
+ for item in value_array:
136
+ item_mapping = _field_as_dict(item)
137
+ item_object = item_mapping.get("valueObject") if item_mapping else None
138
+ if not isinstance(item_object, dict):
139
+ continue
140
+ for name, value in item_object.items():
141
+ child_values.setdefault(name, []).append(_coerce_field_value(value))
142
+ confidence = _coerce_field_confidence(value)
143
+ if confidence is not None:
144
+ child_confidences.setdefault(name, []).append(confidence)
145
+ return {
146
+ f"{field_name}[].{name}": {
147
+ "value": values,
148
+ "confidence": (
149
+ round(sum(child_confidences[name]) / len(child_confidences[name]), 3)
150
+ if child_confidences.get(name)
151
+ else None
152
+ ),
153
+ }
154
+ for name, values in child_values.items()
155
+ }
156
+
157
+
158
+ def extract_fields_from_result(result: Any) -> dict[str, dict[str, Any]]:
159
+ """Flatten result fields for analyzer-test coverage reporting."""
160
+
161
+ extracted: dict[str, dict[str, Any]] = {}
162
+ for content in getattr(result, "contents", None) or []:
163
+ fields = getattr(content, "fields", None)
164
+ if fields is None:
165
+ continue
166
+ if hasattr(fields, "items"):
167
+ items = fields.items()
168
+ elif hasattr(fields, "as_dict"):
169
+ items = fields.as_dict().items()
170
+ else:
171
+ continue
172
+ for name, value in items:
173
+ if name in extracted and extracted[name].get("value") not in (
174
+ None,
175
+ "",
176
+ [],
177
+ {},
178
+ ):
179
+ continue
180
+ extracted[name] = {
181
+ "value": _coerce_field_value(value),
182
+ "confidence": _coerce_field_confidence(value),
183
+ }
184
+ extracted.update(_child_field_entries(name, value))
185
+ return extracted
186
+
187
+
188
+ def _is_populated_value(value: Any) -> bool:
189
+ if value is None or value == "":
190
+ return False
191
+ if isinstance(value, dict):
192
+ return any(_is_populated_value(item) for item in value.values())
193
+ if isinstance(value, list):
194
+ return any(_is_populated_value(item) for item in value)
195
+ return True
196
+
197
+
198
+ def analyzer_test_summary(samples: list[dict[str, Any]]) -> dict[str, Any]:
199
+ """Build deterministic field-coverage and confidence statistics."""
200
+
201
+ low_threshold = 0.6
202
+ total = len(samples)
203
+ succeeded = sum(sample.get("status") == "ok" for sample in samples)
204
+ field_names = {
205
+ name
206
+ for sample in samples
207
+ for name in (sample.get("fields") or {})
208
+ }
209
+ per_field: dict[str, dict[str, Any]] = {}
210
+ for name in sorted(field_names):
211
+ populated = 0
212
+ low = 0
213
+ confidences: list[float] = []
214
+ for sample in samples:
215
+ field = (sample.get("fields") or {}).get(name)
216
+ if field is None or field.get("value") is None:
217
+ continue
218
+ if _is_populated_value(field["value"]):
219
+ populated += 1
220
+ confidence = field.get("confidence")
221
+ if isinstance(confidence, (int, float)):
222
+ confidences.append(float(confidence))
223
+ if confidence < low_threshold:
224
+ low += 1
225
+ per_field[name] = {
226
+ "populated": populated,
227
+ "populatedPct": round(populated / total * 100, 1) if total else 0.0,
228
+ "meanConfidence": (
229
+ round(sum(confidences) / len(confidences), 3)
230
+ if confidences
231
+ else None
232
+ ),
233
+ "lowConfidenceCount": low,
234
+ }
235
+ return {
236
+ "samplesTotal": total,
237
+ "samplesOk": succeeded,
238
+ "samplesFailed": total - succeeded,
239
+ "disclaimer": _TEST_DISCLAIMER,
240
+ "fields": per_field,
241
+ "lowConfidenceThreshold": low_threshold,
242
+ }
243
+
244
+
245
+ def execute_analyzer_test(
246
+ client: Any,
247
+ request: AnalyzerTestRequest,
248
+ *,
249
+ input_plan: InputPlan | None = None,
250
+ run: Callable[[Any, AnalyzeJob], Any] | None = None,
251
+ ) -> dict[str, Any]:
252
+ """Execute analyzer test samples and return a frontend-neutral report."""
253
+
254
+ planned = input_plan or _input_plan(request)
255
+ refs = [str(item.path) for item in planned.inputs]
256
+ jobs = [
257
+ AnalyzeJob(input_ref=ref, analyzer_id=request.name, out_path=None)
258
+ for ref in refs
259
+ ]
260
+ samples: list[dict[str, Any]] = []
261
+
262
+ def collect(outcome: AnalyzeOutcome) -> None:
263
+ if outcome.ok:
264
+ samples.append(
265
+ {
266
+ "input": outcome.job.input_ref,
267
+ "status": "ok",
268
+ "fields": extract_fields_from_result(outcome.result),
269
+ }
270
+ )
271
+ else:
272
+ samples.append(
273
+ {
274
+ "input": outcome.job.input_ref,
275
+ "status": "error",
276
+ "error": str(outcome.error)[:500],
277
+ "fields": {},
278
+ }
279
+ )
280
+
281
+ analyze_many(
282
+ client,
283
+ jobs,
284
+ concurrency=request.concurrency,
285
+ on_result=collect,
286
+ run=run,
287
+ )
288
+ order = {ref: index for index, ref in enumerate(refs)}
289
+ samples.sort(key=lambda sample: order.get(sample["input"], 0))
290
+ return {
291
+ "analyzerId": request.name,
292
+ "summary": analyzer_test_summary(samples),
293
+ "samples": samples,
294
+ }