evalctl 0.3.0__tar.gz → 0.4.2__tar.gz
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.
- {evalctl-0.3.0 → evalctl-0.4.2}/PKG-INFO +54 -15
- {evalctl-0.3.0 → evalctl-0.4.2}/README.md +51 -14
- {evalctl-0.3.0 → evalctl-0.4.2}/evalctl/__init__.py +1 -1
- evalctl-0.4.2/evalctl/artifacts.py +128 -0
- evalctl-0.4.2/evalctl/cli.py +36 -0
- evalctl-0.4.2/evalctl/commands.py +1062 -0
- evalctl-0.4.2/evalctl/doctor.py +193 -0
- evalctl-0.4.2/evalctl/inferctl.py +193 -0
- evalctl-0.4.2/evalctl/integration_contracts.py +21 -0
- evalctl-0.4.2/evalctl/processes.py +86 -0
- evalctl-0.4.2/evalctl/reports.py +57 -0
- evalctl-0.4.2/evalctl/run_state.py +332 -0
- evalctl-0.4.2/evalctl/runner.py +373 -0
- evalctl-0.4.2/evalctl/scoring.py +281 -0
- evalctl-0.4.2/evalctl/spoolctl.py +208 -0
- evalctl-0.4.2/evalctl/static_contract.py +626 -0
- evalctl-0.4.2/evalctl/suite.py +226 -0
- {evalctl-0.3.0 → evalctl-0.4.2}/evalctl.egg-info/PKG-INFO +54 -15
- evalctl-0.4.2/evalctl.egg-info/SOURCES.txt +27 -0
- evalctl-0.4.2/evalctl.egg-info/requires.txt +3 -0
- {evalctl-0.3.0 → evalctl-0.4.2}/pyproject.toml +14 -1
- {evalctl-0.3.0 → evalctl-0.4.2}/tests/test_evalctl.py +1090 -155
- evalctl-0.4.2/tests/test_real_spoolctl.py +326 -0
- evalctl-0.3.0/evalctl/cli.py +0 -2352
- evalctl-0.3.0/evalctl.egg-info/SOURCES.txt +0 -12
- {evalctl-0.3.0 → evalctl-0.4.2}/LICENSE +0 -0
- {evalctl-0.3.0 → evalctl-0.4.2}/evalctl/__main__.py +0 -0
- {evalctl-0.3.0 → evalctl-0.4.2}/evalctl.egg-info/dependency_links.txt +0 -0
- {evalctl-0.3.0 → evalctl-0.4.2}/evalctl.egg-info/entry_points.txt +0 -0
- {evalctl-0.3.0 → evalctl-0.4.2}/evalctl.egg-info/top_level.txt +0 -0
- {evalctl-0.3.0 → evalctl-0.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evalctl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Local-first evals for agents, not just prompts.
|
|
5
5
|
Author: evalctl contributors
|
|
6
6
|
License: Apache License
|
|
@@ -217,6 +217,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
217
217
|
Requires-Python: >=3.11
|
|
218
218
|
Description-Content-Type: text/markdown
|
|
219
219
|
License-File: LICENSE
|
|
220
|
+
Provides-Extra: dev
|
|
221
|
+
Requires-Dist: coverage>=7; extra == "dev"
|
|
220
222
|
Dynamic: license-file
|
|
221
223
|
|
|
222
224
|
# evalctl
|
|
@@ -232,11 +234,13 @@ durable artifacts. It scores what agents actually do — files written, diffs
|
|
|
232
234
|
produced, commands run — on your own machine, with no gateway, dashboard, or
|
|
233
235
|
SaaS account.
|
|
234
236
|
|
|
235
|
-
v0.
|
|
236
|
-
inspection,
|
|
237
|
+
v0.4 writes durable run metadata, supports crash resume, adds local run-state
|
|
238
|
+
inspection, diagnoses runtime health, produces side-effect-free execution plans,
|
|
239
|
+
and can optionally delegate runner execution to
|
|
237
240
|
[spoolctl](https://github.com/Ozhiaki/spoolctl). The standalone synchronous path
|
|
238
241
|
remains complete and requires no external service. [inferctl](https://inferctl.dev)
|
|
239
|
-
|
|
242
|
+
preflight provenance can be captured before runner execution without changing
|
|
243
|
+
report scoring.
|
|
240
244
|
|
|
241
245
|
## About
|
|
242
246
|
|
|
@@ -259,17 +263,17 @@ evalctl is agent-shaped.
|
|
|
259
263
|
| --- | --- | --- |
|
|
260
264
|
| Unit under test | prompt → completion | agent run → files, diffs, command logs, artifacts |
|
|
261
265
|
| Scoring surface | text of a response | resulting workspace: git diff, expected/forbidden file changes, exit codes, plus text |
|
|
262
|
-
| Execution | in-process, synchronous | v0.
|
|
263
|
-
| Model context | provider API keys | [inferctl](https://inferctl.dev)
|
|
266
|
+
| Execution | in-process, synchronous | v0.4 synchronous by default; `plan` previews actions, `doctor` diagnoses state, `run --resume` resumes crashed runs; optional `--queue spoolctl` delegates runner execution |
|
|
267
|
+
| Model context | provider API keys | Optional [inferctl](https://inferctl.dev) preflight provenance through `run --inferctl-task`; route capture remains deferred |
|
|
264
268
|
|
|
265
269
|
## Status
|
|
266
270
|
|
|
267
|
-
Python pre-release. v0.
|
|
268
|
-
execution, durable run metadata, crash resume, local jobs
|
|
269
|
-
spoolctl queueing,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
directory.
|
|
271
|
+
Python pre-release. v0.4 provides scaffold, validate, doctor, plan, bounded
|
|
272
|
+
parallel run execution, durable run metadata, crash resume, bounded local jobs
|
|
273
|
+
inspection, optional spoolctl queueing, optional inferctl preflight provenance,
|
|
274
|
+
status, report, deterministic local scorers, CLI authoring verbs, execution
|
|
275
|
+
replay for failed cases, command scorers, truthful warnings/errors, real schema
|
|
276
|
+
output, and artifact replay from a copied run directory.
|
|
273
277
|
`contract_version` remains `1`.
|
|
274
278
|
|
|
275
279
|
## Install
|
|
@@ -300,8 +304,11 @@ evalctl suite add demo --runner-argv "python3 $EVALCTL_WORKSPACE/r.py" --json
|
|
|
300
304
|
evalctl case add demo --task "do X" --workspace fixtures/x --expect-json '{"exact":"ok"}' --json
|
|
301
305
|
evalctl scorer add demo --name exact --required --json
|
|
302
306
|
evalctl run demo --json
|
|
307
|
+
evalctl doctor --json
|
|
308
|
+
evalctl plan demo --json
|
|
309
|
+
evalctl run demo --inferctl-task code --json
|
|
303
310
|
evalctl run --resume <run-id> --json
|
|
304
|
-
evalctl jobs list --json
|
|
311
|
+
evalctl jobs list --limit 50 --json
|
|
305
312
|
evalctl run demo --queue spoolctl --slots 4 --json
|
|
306
313
|
evalctl replay --failed <run-id> --json
|
|
307
314
|
evalctl report <run-id> --format json
|
|
@@ -322,23 +329,55 @@ reservation is reclaimed by explicit `--resume`. `jobs list|get|prune` inspects
|
|
|
322
329
|
completed, running, stale, and orphaned local run state and safely prunes only
|
|
323
330
|
with explicit confirmation.
|
|
324
331
|
|
|
332
|
+
`jobs list` is bounded by default. Use `--limit` and `--cursor` to page through
|
|
333
|
+
large run directories; the JSON envelope includes pagination metadata and a
|
|
334
|
+
paste-ready next-page command when more rows are available.
|
|
335
|
+
|
|
325
336
|
Durability sidecars are operational state. Reports and artifact replay do not
|
|
326
337
|
require `run.json`, `.reservation.json`, `.spoolctl.db`, `state.json`, or
|
|
327
338
|
`job.json`; `report_hash` stays based on the report projection. `SOURCE_DATE_EPOCH`
|
|
328
339
|
controls `created_ts` for deterministic manifest parity.
|
|
329
340
|
|
|
341
|
+
## Doctor And Plan
|
|
342
|
+
|
|
343
|
+
`evalctl doctor --json` reports runtime, suite root, runs root, reservations,
|
|
344
|
+
spoolctl, inferctl, and runner-safety state without failing just because a
|
|
345
|
+
component is degraded. Use `--component NAME` to scope diagnostics and `--fast`
|
|
346
|
+
for PATH-only optional integration checks.
|
|
347
|
+
|
|
348
|
+
`evalctl plan <suite> --json` resolves the case set without creating run
|
|
349
|
+
directories, enqueueing jobs, executing runners, scoring, or writing inferctl
|
|
350
|
+
artifacts. The plan includes run-id strategy, execution mode, independent-case
|
|
351
|
+
dependency metadata, parallel tracks, per-case actions, warnings, and
|
|
352
|
+
paste-ready follow-up commands. `--resume`, `--queue spoolctl`, `--slots`, and
|
|
353
|
+
`--inferctl-task` mirror the run surface for planning.
|
|
354
|
+
|
|
330
355
|
## Optional Spoolctl Queue
|
|
331
356
|
|
|
332
357
|
`evalctl run <suite> --queue spoolctl --json` delegates only runner execution to
|
|
333
|
-
spoolctl (`>= 0.4.
|
|
358
|
+
spoolctl (`>= 0.4.11`, speaking contract `>= 2`). A spoolctl reporting a newer
|
|
359
|
+
contract is accepted. Evalctl still prepares workspaces, normalizes stdout and
|
|
334
360
|
stderr, captures workspace diffs, scores cases, and writes terminal markers. If
|
|
335
361
|
spoolctl is absent or incompatible, queued runs fail explicitly; non-queued runs
|
|
336
362
|
do not need spoolctl.
|
|
337
363
|
|
|
338
|
-
The queue database is per-run at `.spoolctl.db`; v0.
|
|
364
|
+
The queue database is per-run at `.spoolctl.db`; v0.4 starts one ephemeral
|
|
339
365
|
`spoolctl work --drain` worker per queued run. General externally managed worker
|
|
340
366
|
fleets are not part of this release.
|
|
341
367
|
|
|
368
|
+
## Inferctl Preflight Provenance
|
|
369
|
+
|
|
370
|
+
`evalctl run <suite> --inferctl-task TASK --json` probes inferctl once per run
|
|
371
|
+
and, when compatible preflight support is available, writes per-case
|
|
372
|
+
`inferctl-preflight.json` and `inferctl-provenance.json` before runner execution.
|
|
373
|
+
Queued spoolctl runs capture the same artifacts before enqueue. Absence,
|
|
374
|
+
incompatibility, parse failures, timeouts, and policy/readiness blocks are
|
|
375
|
+
warnings; the runner still executes and scoring proceeds.
|
|
376
|
+
|
|
377
|
+
The v0.4 capture mode is `preflight` only. `inferctl route` is not called, and
|
|
378
|
+
report projection is unchanged, so `report_hash` remains comparable to an
|
|
379
|
+
equivalent run without inferctl.
|
|
380
|
+
|
|
342
381
|
## Authoring
|
|
343
382
|
|
|
344
383
|
`suite add`, `case add`, and `scorer add` let agents build a suite without
|
|
@@ -11,11 +11,13 @@ durable artifacts. It scores what agents actually do — files written, diffs
|
|
|
11
11
|
produced, commands run — on your own machine, with no gateway, dashboard, or
|
|
12
12
|
SaaS account.
|
|
13
13
|
|
|
14
|
-
v0.
|
|
15
|
-
inspection,
|
|
14
|
+
v0.4 writes durable run metadata, supports crash resume, adds local run-state
|
|
15
|
+
inspection, diagnoses runtime health, produces side-effect-free execution plans,
|
|
16
|
+
and can optionally delegate runner execution to
|
|
16
17
|
[spoolctl](https://github.com/Ozhiaki/spoolctl). The standalone synchronous path
|
|
17
18
|
remains complete and requires no external service. [inferctl](https://inferctl.dev)
|
|
18
|
-
|
|
19
|
+
preflight provenance can be captured before runner execution without changing
|
|
20
|
+
report scoring.
|
|
19
21
|
|
|
20
22
|
## About
|
|
21
23
|
|
|
@@ -38,17 +40,17 @@ evalctl is agent-shaped.
|
|
|
38
40
|
| --- | --- | --- |
|
|
39
41
|
| Unit under test | prompt → completion | agent run → files, diffs, command logs, artifacts |
|
|
40
42
|
| Scoring surface | text of a response | resulting workspace: git diff, expected/forbidden file changes, exit codes, plus text |
|
|
41
|
-
| Execution | in-process, synchronous | v0.
|
|
42
|
-
| Model context | provider API keys | [inferctl](https://inferctl.dev)
|
|
43
|
+
| Execution | in-process, synchronous | v0.4 synchronous by default; `plan` previews actions, `doctor` diagnoses state, `run --resume` resumes crashed runs; optional `--queue spoolctl` delegates runner execution |
|
|
44
|
+
| Model context | provider API keys | Optional [inferctl](https://inferctl.dev) preflight provenance through `run --inferctl-task`; route capture remains deferred |
|
|
43
45
|
|
|
44
46
|
## Status
|
|
45
47
|
|
|
46
|
-
Python pre-release. v0.
|
|
47
|
-
execution, durable run metadata, crash resume, local jobs
|
|
48
|
-
spoolctl queueing,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
directory.
|
|
48
|
+
Python pre-release. v0.4 provides scaffold, validate, doctor, plan, bounded
|
|
49
|
+
parallel run execution, durable run metadata, crash resume, bounded local jobs
|
|
50
|
+
inspection, optional spoolctl queueing, optional inferctl preflight provenance,
|
|
51
|
+
status, report, deterministic local scorers, CLI authoring verbs, execution
|
|
52
|
+
replay for failed cases, command scorers, truthful warnings/errors, real schema
|
|
53
|
+
output, and artifact replay from a copied run directory.
|
|
52
54
|
`contract_version` remains `1`.
|
|
53
55
|
|
|
54
56
|
## Install
|
|
@@ -79,8 +81,11 @@ evalctl suite add demo --runner-argv "python3 $EVALCTL_WORKSPACE/r.py" --json
|
|
|
79
81
|
evalctl case add demo --task "do X" --workspace fixtures/x --expect-json '{"exact":"ok"}' --json
|
|
80
82
|
evalctl scorer add demo --name exact --required --json
|
|
81
83
|
evalctl run demo --json
|
|
84
|
+
evalctl doctor --json
|
|
85
|
+
evalctl plan demo --json
|
|
86
|
+
evalctl run demo --inferctl-task code --json
|
|
82
87
|
evalctl run --resume <run-id> --json
|
|
83
|
-
evalctl jobs list --json
|
|
88
|
+
evalctl jobs list --limit 50 --json
|
|
84
89
|
evalctl run demo --queue spoolctl --slots 4 --json
|
|
85
90
|
evalctl replay --failed <run-id> --json
|
|
86
91
|
evalctl report <run-id> --format json
|
|
@@ -101,23 +106,55 @@ reservation is reclaimed by explicit `--resume`. `jobs list|get|prune` inspects
|
|
|
101
106
|
completed, running, stale, and orphaned local run state and safely prunes only
|
|
102
107
|
with explicit confirmation.
|
|
103
108
|
|
|
109
|
+
`jobs list` is bounded by default. Use `--limit` and `--cursor` to page through
|
|
110
|
+
large run directories; the JSON envelope includes pagination metadata and a
|
|
111
|
+
paste-ready next-page command when more rows are available.
|
|
112
|
+
|
|
104
113
|
Durability sidecars are operational state. Reports and artifact replay do not
|
|
105
114
|
require `run.json`, `.reservation.json`, `.spoolctl.db`, `state.json`, or
|
|
106
115
|
`job.json`; `report_hash` stays based on the report projection. `SOURCE_DATE_EPOCH`
|
|
107
116
|
controls `created_ts` for deterministic manifest parity.
|
|
108
117
|
|
|
118
|
+
## Doctor And Plan
|
|
119
|
+
|
|
120
|
+
`evalctl doctor --json` reports runtime, suite root, runs root, reservations,
|
|
121
|
+
spoolctl, inferctl, and runner-safety state without failing just because a
|
|
122
|
+
component is degraded. Use `--component NAME` to scope diagnostics and `--fast`
|
|
123
|
+
for PATH-only optional integration checks.
|
|
124
|
+
|
|
125
|
+
`evalctl plan <suite> --json` resolves the case set without creating run
|
|
126
|
+
directories, enqueueing jobs, executing runners, scoring, or writing inferctl
|
|
127
|
+
artifacts. The plan includes run-id strategy, execution mode, independent-case
|
|
128
|
+
dependency metadata, parallel tracks, per-case actions, warnings, and
|
|
129
|
+
paste-ready follow-up commands. `--resume`, `--queue spoolctl`, `--slots`, and
|
|
130
|
+
`--inferctl-task` mirror the run surface for planning.
|
|
131
|
+
|
|
109
132
|
## Optional Spoolctl Queue
|
|
110
133
|
|
|
111
134
|
`evalctl run <suite> --queue spoolctl --json` delegates only runner execution to
|
|
112
|
-
spoolctl (`>= 0.4.
|
|
135
|
+
spoolctl (`>= 0.4.11`, speaking contract `>= 2`). A spoolctl reporting a newer
|
|
136
|
+
contract is accepted. Evalctl still prepares workspaces, normalizes stdout and
|
|
113
137
|
stderr, captures workspace diffs, scores cases, and writes terminal markers. If
|
|
114
138
|
spoolctl is absent or incompatible, queued runs fail explicitly; non-queued runs
|
|
115
139
|
do not need spoolctl.
|
|
116
140
|
|
|
117
|
-
The queue database is per-run at `.spoolctl.db`; v0.
|
|
141
|
+
The queue database is per-run at `.spoolctl.db`; v0.4 starts one ephemeral
|
|
118
142
|
`spoolctl work --drain` worker per queued run. General externally managed worker
|
|
119
143
|
fleets are not part of this release.
|
|
120
144
|
|
|
145
|
+
## Inferctl Preflight Provenance
|
|
146
|
+
|
|
147
|
+
`evalctl run <suite> --inferctl-task TASK --json` probes inferctl once per run
|
|
148
|
+
and, when compatible preflight support is available, writes per-case
|
|
149
|
+
`inferctl-preflight.json` and `inferctl-provenance.json` before runner execution.
|
|
150
|
+
Queued spoolctl runs capture the same artifacts before enqueue. Absence,
|
|
151
|
+
incompatibility, parse failures, timeouts, and policy/readiness blocks are
|
|
152
|
+
warnings; the runner still executes and scoring proceeds.
|
|
153
|
+
|
|
154
|
+
The v0.4 capture mode is `preflight` only. `inferctl route` is not called, and
|
|
155
|
+
report projection is unchanged, so `report_hash` remains comparable to an
|
|
156
|
+
equivalent run without inferctl.
|
|
157
|
+
|
|
121
158
|
## Authoring
|
|
122
159
|
|
|
123
160
|
`suite add`, `case add`, and `scorer add` let agents build a suite without
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import stat
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .static_contract import EvalctlError, sha256_bytes, sha256_text, stable_json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_json(path: Path) -> Any:
|
|
15
|
+
try:
|
|
16
|
+
return json.loads(path.read_text())
|
|
17
|
+
except FileNotFoundError:
|
|
18
|
+
raise EvalctlError("E_SUITE_NOT_FOUND", f"missing file: {path}", f"create {path} or run evalctl init", 1)
|
|
19
|
+
except json.JSONDecodeError as exc:
|
|
20
|
+
raise EvalctlError("E_SCHEMA_VIOLATION", f"invalid JSON in {path}: {exc.msg}", f"fix {path}:{exc.lineno}", 1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_cases(cases_path: Path) -> list[dict[str, Any]]:
|
|
24
|
+
cases: list[dict[str, Any]] = []
|
|
25
|
+
seen: set[str] = set()
|
|
26
|
+
try:
|
|
27
|
+
lines = cases_path.read_text().splitlines()
|
|
28
|
+
except FileNotFoundError:
|
|
29
|
+
raise EvalctlError("E_SUITE_NOT_FOUND", f"missing file: {cases_path}", f"create {cases_path}", 1)
|
|
30
|
+
for line_no, line in enumerate(lines, 1):
|
|
31
|
+
if not line.strip():
|
|
32
|
+
continue
|
|
33
|
+
try:
|
|
34
|
+
case = json.loads(line)
|
|
35
|
+
except json.JSONDecodeError as exc:
|
|
36
|
+
raise EvalctlError("E_CASE_INVALID", f"invalid JSONL at {cases_path}:{line_no}: {exc.msg}", f"fix line {line_no}", 1)
|
|
37
|
+
case_id = case.get("id") or sha256_text(stable_json(case))[7:19]
|
|
38
|
+
if case_id in seen:
|
|
39
|
+
raise EvalctlError("E_CASE_INVALID", f"duplicate case id: {case_id}", "choose unique case ids", 1)
|
|
40
|
+
seen.add(case_id)
|
|
41
|
+
case["id"] = case_id
|
|
42
|
+
cases.append(case)
|
|
43
|
+
return cases
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normalize_rel(path: Path, root: Path) -> str:
|
|
47
|
+
rel = path.relative_to(root)
|
|
48
|
+
parts = rel.parts
|
|
49
|
+
posix = PurePosixPath(*parts).as_posix()
|
|
50
|
+
if posix in ("", ".") or ".." in parts:
|
|
51
|
+
raise ValueError("invalid relative path")
|
|
52
|
+
posix.encode("utf-8", "strict")
|
|
53
|
+
return posix
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def display_path_name(path: Path) -> str:
|
|
57
|
+
return os.fsencode(path.name).decode("utf-8", "replace")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def manifest(root: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
61
|
+
warnings: list[dict[str, Any]] = []
|
|
62
|
+
entries: list[dict[str, Any]] = []
|
|
63
|
+
for path in sorted(root.rglob("*"), key=os.fsencode):
|
|
64
|
+
try:
|
|
65
|
+
rel = normalize_rel(path, root)
|
|
66
|
+
st = path.lstat()
|
|
67
|
+
if stat.S_ISDIR(st.st_mode):
|
|
68
|
+
entries.append({"path": rel, "kind": "directory"})
|
|
69
|
+
elif stat.S_ISLNK(st.st_mode):
|
|
70
|
+
target = os.readlink(path)
|
|
71
|
+
entries.append({"path": rel, "kind": "symlink", "target": target, "sha256": sha256_bytes(target.encode("utf-8")), "broken": not path.exists()})
|
|
72
|
+
elif stat.S_ISREG(st.st_mode):
|
|
73
|
+
entries.append({"path": rel, "kind": "file", "sha256": sha256_bytes(path.read_bytes()), "size": st.st_size})
|
|
74
|
+
else:
|
|
75
|
+
subtype = "fifo" if stat.S_ISFIFO(st.st_mode) else "socket" if stat.S_ISSOCK(st.st_mode) else "device" if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode) else "unknown"
|
|
76
|
+
entries.append({"path": rel, "kind": "other", "subtype": subtype})
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
warnings.append({"code": "W_PATH_UNREADABLE", "message": f"could not read path {display_path_name(path)}: {exc}"})
|
|
79
|
+
return {"root": ".", "entries": entries}, warnings
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def diff_manifests(before: dict[str, Any], after: dict[str, Any]) -> dict[str, Any]:
|
|
83
|
+
b = {e["path"]: e for e in before["entries"]}
|
|
84
|
+
a = {e["path"]: e for e in after["entries"]}
|
|
85
|
+
changed = []
|
|
86
|
+
for path in sorted(set(b) | set(a)):
|
|
87
|
+
if path not in b:
|
|
88
|
+
changed.append({"path": path, "status": "added", "kind": a[path]["kind"], "sha256_before": None, "sha256_after": a[path].get("sha256")})
|
|
89
|
+
elif path not in a:
|
|
90
|
+
changed.append({"path": path, "status": "deleted", "kind": b[path]["kind"], "sha256_before": b[path].get("sha256"), "sha256_after": None})
|
|
91
|
+
elif b[path].get("sha256") != a[path].get("sha256") or b[path].get("kind") != a[path].get("kind"):
|
|
92
|
+
changed.append({"path": path, "status": "modified", "kind": a[path]["kind"], "sha256_before": b[path].get("sha256"), "sha256_after": a[path].get("sha256")})
|
|
93
|
+
return {"changed_paths": changed}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def apply_redaction(text: str, patterns: list[str], values: list[str]) -> tuple[str, bool]:
|
|
97
|
+
changed = False
|
|
98
|
+
for value in values:
|
|
99
|
+
if value and value in text:
|
|
100
|
+
text = text.replace(value, "[REDACTED]")
|
|
101
|
+
changed = True
|
|
102
|
+
for pattern in patterns:
|
|
103
|
+
text2 = re.sub(pattern, "[REDACTED]", text)
|
|
104
|
+
changed = changed or text2 != text
|
|
105
|
+
text = text2
|
|
106
|
+
return text, changed
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _atomic_write(path: Path, text: str, *, _writer: Any | None = None) -> None:
|
|
110
|
+
writer = _writer or (lambda tmp_path, value: tmp_path.write_text(value))
|
|
111
|
+
tmp_name = None
|
|
112
|
+
with tempfile.NamedTemporaryFile("w", dir=path.parent, prefix=f".{path.name}.", suffix=".tmp", delete=False) as tmp:
|
|
113
|
+
tmp_name = tmp.name
|
|
114
|
+
tmp_path = Path(tmp_name)
|
|
115
|
+
try:
|
|
116
|
+
writer(tmp_path, text)
|
|
117
|
+
os.replace(tmp_path, path)
|
|
118
|
+
finally:
|
|
119
|
+
if tmp_path.exists():
|
|
120
|
+
tmp_path.unlink()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def write_json(path: Path, data: Any) -> None:
|
|
124
|
+
_atomic_write(path, json.dumps(data, indent=2, sort_keys=True) + "\n")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def render_text_diff(diff: dict[str, Any]) -> str:
|
|
128
|
+
return "\n".join(f"{p['status']}\t{p['path']}" for p in diff["changed_paths"]) + ("\n" if diff["changed_paths"] else "")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from .commands import dispatch, report_format_json_requested, wants_json
|
|
7
|
+
from .static_contract import EvalctlError, envelope, stable_json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def print_error(err: EvalctlError, *, json_mode: bool, started: float | None = None) -> int:
|
|
11
|
+
print(err.error["message"], file=sys.stderr)
|
|
12
|
+
if err.error.get("corrected_command"):
|
|
13
|
+
print(f"Did you mean: {err.error['corrected_command']}", file=sys.stderr)
|
|
14
|
+
elif err.error.get("did_you_mean"):
|
|
15
|
+
print(f"Did you mean: {err.error['did_you_mean']}", file=sys.stderr)
|
|
16
|
+
if json_mode:
|
|
17
|
+
print(stable_json(envelope(None, ok=False, errors=[err.error], started=started)))
|
|
18
|
+
return err.exit_code
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: list[str] | None = None) -> int:
|
|
22
|
+
started = time.time()
|
|
23
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
24
|
+
json_mode = wants_json(argv)
|
|
25
|
+
try:
|
|
26
|
+
return dispatch(argv, json_mode, started)
|
|
27
|
+
except EvalctlError as exc:
|
|
28
|
+
return print_error(exc, json_mode=json_mode or report_format_json_requested(argv), started=started)
|
|
29
|
+
except KeyboardInterrupt:
|
|
30
|
+
return print_error(EvalctlError("E_RUNNER_FAILED", "interrupted", "retry the command", 3), json_mode=json_mode or report_format_json_requested(argv), started=started)
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
return print_error(EvalctlError("E_RUNNER_FAILED", f"internal error: {exc}", "run with --json and inspect errors[0]", 3), json_mode=json_mode or report_format_json_requested(argv), started=started)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
raise SystemExit(main())
|