bug-age 2.0.0__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.
- bug_age/__init__.py +13 -0
- bug_age/__main__.py +7 -0
- bug_age/_compat.py +155 -0
- bug_age/bundle.py +381 -0
- bug_age/catalog.py +68 -0
- bug_age/cli.py +265 -0
- bug_age/data/Dockerfile +14 -0
- bug_age/data/bundle.schema.json +89 -0
- bug_age/data/explorer.html +851 -0
- bug_age/explorer.py +42 -0
- bug_age/feasibility.py +339 -0
- bug_age/git.py +71 -0
- bug_age/mcp.py +211 -0
- bug_age/pipeline.py +151 -0
- bug_age/repros.py +178 -0
- bug_age/runner.py +294 -0
- bug_age/schema.py +102 -0
- bug_age/sweep.py +280 -0
- bug_age/szz.py +135 -0
- bug_age/timetravel.py +108 -0
- bug_age-2.0.0.dist-info/METADATA +301 -0
- bug_age-2.0.0.dist-info/RECORD +27 -0
- bug_age-2.0.0.dist-info/WHEEL +5 -0
- bug_age-2.0.0.dist-info/entry_points.txt +2 -0
- bug_age-2.0.0.dist-info/licenses/LICENSE-APACHE +202 -0
- bug_age-2.0.0.dist-info/licenses/LICENSE-MIT +21 -0
- bug_age-2.0.0.dist-info/top_level.txt +1 -0
bug_age/cli.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT OR Apache-2.0
|
|
2
|
+
"""bug-age command-line interface.
|
|
3
|
+
|
|
4
|
+
One command, one subcommand per pipeline stage. Each subcommand reads files, writes one JSON output
|
|
5
|
+
(stdout or `--out`), and logs a one-line summary to stderr. Exit status is 0 on success, 1 on a
|
|
6
|
+
runtime error and 2 on a usage error.
|
|
7
|
+
"""
|
|
8
|
+
import hashlib
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import __version__, bundle, catalog, explorer, mcp, szz
|
|
15
|
+
from ._compat import dumps, get, lines, loads
|
|
16
|
+
|
|
17
|
+
USAGE = """usage: bug-age [--version | --help]
|
|
18
|
+
bug-age catalog --repo DIR --path CRATE [--head REV] [--out FILE]
|
|
19
|
+
bug-age szz --repo DIR --catalog FILE [--window-start YYYY-MM-DD] [--release-tags GLOB] [--out FILE]
|
|
20
|
+
bug-age sweep --repo DIR --path CRATE --fix SHA --overlay DIR --test CMD
|
|
21
|
+
[--build CMD] [--start SHA] [--runner docker|local] [--repeats N] [--image IMG]
|
|
22
|
+
[--time-travel-script FILE|none] [--scratch DIR] [--logs DIR] [--out FILE]
|
|
23
|
+
[--namespace NAME] [--bucket N (toolchain minor rounding, default 4; 0 = exact)]
|
|
24
|
+
bug-age probe --repo DIR --path CRATE --commits SHA,SHA,... --overlay DIR --test CMD [same options]
|
|
25
|
+
bug-age bundle --repo DIR --results DIR --repo-url URL --out DIR [--spike DIR] [--out-of-scope 'STAGE|EVIDENCE|DOES_NOT_PROVE']
|
|
26
|
+
bug-age explorer [--page FILE] --targets NAME=DIR,NAME=DIR --out DIR (DIR = a bundle output dir)
|
|
27
|
+
bug-age mcp --bundles NAME=FILE,NAME=FILE (read-only MCP server on stdio over bundle.json files)
|
|
28
|
+
bug-age pipeline TARGET.toml [--fresh] [--skip-feasibility] (every stage for one target)
|
|
29
|
+
bug-age dockerfile (print the Method B image's Dockerfile: bug-age dockerfile | docker build -t bug-age-spike:4 -)"""
|
|
30
|
+
|
|
31
|
+
WARN_BYTES = 8 * 1024 * 1024
|
|
32
|
+
MAX_BYTES = 16 * 1024 * 1024
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Failure(Exception):
|
|
36
|
+
"""A runtime error: printed as `bug-age: <message>`, exit status 1."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def flags(args):
|
|
40
|
+
"""`--key value` pairs as a dict; anything else is an error."""
|
|
41
|
+
out, it = {}, iter(args)
|
|
42
|
+
for k in it:
|
|
43
|
+
if not k.startswith("--"):
|
|
44
|
+
raise Failure(f"unexpected argument '{k}'")
|
|
45
|
+
try:
|
|
46
|
+
out[k[2:]] = next(it)
|
|
47
|
+
except StopIteration:
|
|
48
|
+
raise Failure(f"missing value for '{k}'") from None
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def need(f, key):
|
|
53
|
+
"""A required flag's value."""
|
|
54
|
+
if key not in f:
|
|
55
|
+
raise Failure(f"missing --{key}")
|
|
56
|
+
return f[key]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def write(path, text):
|
|
60
|
+
"""Write UTF-8 text with LF line ends."""
|
|
61
|
+
try:
|
|
62
|
+
with open(path, "w", encoding="utf-8", newline="\n") as fh:
|
|
63
|
+
fh.write(text)
|
|
64
|
+
except OSError as exc:
|
|
65
|
+
raise Failure(f"cannot write {path}: {exc}") from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def emit(value, out):
|
|
69
|
+
"""Pretty JSON to `out`, or to stdout when no file is given."""
|
|
70
|
+
text = dumps(value, pretty=True) + "\n"
|
|
71
|
+
if out is None:
|
|
72
|
+
sys.stdout.write(text)
|
|
73
|
+
else:
|
|
74
|
+
write(out, text)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def sha256(text):
|
|
78
|
+
"""Hex SHA-256 of a text's UTF-8 bytes."""
|
|
79
|
+
return hashlib.sha256(text.encode()).hexdigest()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def tool_version(cmd, args):
|
|
83
|
+
"""First line a tool prints for its version, or "unavailable"."""
|
|
84
|
+
try:
|
|
85
|
+
out = subprocess.run([cmd, *args], capture_output=True).stdout.decode("utf-8", "replace")
|
|
86
|
+
except OSError:
|
|
87
|
+
return "unavailable"
|
|
88
|
+
first = lines(out)
|
|
89
|
+
return first[0] if first else ""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def command_line():
|
|
93
|
+
"""This run's command line as it may be published (see bundle.public_command)."""
|
|
94
|
+
return bundle.public_command(sys.argv, os.getcwd(), os.environ.get("HOME", ""))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def read_json(path):
|
|
98
|
+
"""A JSON file as a value, with the path in any error."""
|
|
99
|
+
try:
|
|
100
|
+
return loads(Path(path).read_text(encoding="utf-8"))
|
|
101
|
+
except (OSError, ValueError) as exc:
|
|
102
|
+
raise Failure(f"{path}: {exc}") from exc
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cmd_catalog(f):
|
|
106
|
+
"""`catalog`: rule-based list of fix commits."""
|
|
107
|
+
cat = catalog.build(Path(need(f, "repo")), f.get("head", "HEAD"), need(f, "path"))
|
|
108
|
+
print(f"catalog: {len(cat['fixes'])} fixes, {len(cat['excluded'])} excluded, {cat['scanned']} commits scanned",
|
|
109
|
+
file=sys.stderr)
|
|
110
|
+
emit(cat, f.get("out"))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cmd_szz(f):
|
|
114
|
+
"""`szz`: Method A for every catalogued fix."""
|
|
115
|
+
repo = Path(need(f, "repo"))
|
|
116
|
+
cat = read_json(need(f, "catalog"))
|
|
117
|
+
path = get(cat, "path")
|
|
118
|
+
if not isinstance(path, str):
|
|
119
|
+
raise Failure("catalog has no path")
|
|
120
|
+
window = f.get("window-start")
|
|
121
|
+
release_tags = f.get("release-tags", "v[0-9]*")
|
|
122
|
+
fixes = get(cat, "fixes")
|
|
123
|
+
if not isinstance(fixes, list):
|
|
124
|
+
raise Failure("catalog has no fixes")
|
|
125
|
+
bugs = []
|
|
126
|
+
for e in fixes:
|
|
127
|
+
def text(k):
|
|
128
|
+
v = get(e, k)
|
|
129
|
+
return v if isinstance(v, str) else ""
|
|
130
|
+
parents = get(e, "parents")
|
|
131
|
+
entry = {"sha": text("sha"), "date": text("date"), "subject": text("subject"), "rule": text("rule"),
|
|
132
|
+
"parents": [p for p in parents if isinstance(p, str)] if isinstance(parents, list) else []}
|
|
133
|
+
bugs.append(szz.analyze(repo, entry, path, window, release_tags))
|
|
134
|
+
with_intro = sum(1 for b in bugs if b["lifetime"] is not None)
|
|
135
|
+
in_window = sum(1 for b in bugs if b["in_window"] is True)
|
|
136
|
+
print(f"szz: {len(bugs)} fixes, {with_intro} with a nominated introducer, {in_window} in window", file=sys.stderr)
|
|
137
|
+
summary = {"fixes": len(bugs), "with_introducer": with_intro, "in_window": in_window if window is not None else None,
|
|
138
|
+
"window_start": window, "release_tags": release_tags}
|
|
139
|
+
emit({"head": get(cat, "head"), "path": path, "method": "A (SZZ-style, git blame -w on first parent)",
|
|
140
|
+
"summary": summary, "bugs": bugs}, f.get("out"))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_bundle(f):
|
|
144
|
+
"""`bundle`: the canonical data bundle, its JS wrapper and a receipt."""
|
|
145
|
+
repo, out = Path(need(f, "repo")), Path(need(f, "out"))
|
|
146
|
+
spike = Path(f.get("spike", "spike"))
|
|
147
|
+
oos = None
|
|
148
|
+
if "out-of-scope" in f:
|
|
149
|
+
parts = f["out-of-scope"].split("|", 2)
|
|
150
|
+
if len(parts) != 3:
|
|
151
|
+
raise Failure("--out-of-scope must be 'STAGE|EVIDENCE|DOES_NOT_PROVE'")
|
|
152
|
+
oos = tuple(parts)
|
|
153
|
+
try:
|
|
154
|
+
built = bundle.build(repo, Path(need(f, "results")), spike, need(f, "repo-url"), oos)
|
|
155
|
+
except bundle.BundleError as exc:
|
|
156
|
+
raise Failure(str(exc)) from exc
|
|
157
|
+
text = dumps(built)
|
|
158
|
+
js = f"window.BUG_AGE_BUNDLE = {text};\n"
|
|
159
|
+
if len(js.encode()) > MAX_BYTES:
|
|
160
|
+
raise Failure(f"size gate failed: bundle.js is {len(js.encode())} bytes (> {MAX_BYTES})")
|
|
161
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
write(out / "bundle.json", text)
|
|
163
|
+
write(out / "bundle.js", js)
|
|
164
|
+
receipt = {
|
|
165
|
+
"command": command_line(),
|
|
166
|
+
"schema_version": bundle.SCHEMA_VERSION,
|
|
167
|
+
"blameless_gate": "PASS",
|
|
168
|
+
"size": {"bundle_json_bytes": len(text.encode()), "bundle_js_bytes": len(js.encode()),
|
|
169
|
+
"warn_over_8mb": len(js.encode()) > WARN_BYTES},
|
|
170
|
+
"outputs": {"bundle.json": sha256(text), "bundle.js": sha256(js)},
|
|
171
|
+
"versions": {"bug-age": __version__, "git": tool_version("git", ["--version"])},
|
|
172
|
+
"warnings": [],
|
|
173
|
+
}
|
|
174
|
+
print(f"bundle: {len(text.encode())} bytes, sha256 {sha256(text)}, blameless gate PASS", file=sys.stderr)
|
|
175
|
+
emit(receipt, out / "receipt.json")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def cmd_explorer(f):
|
|
179
|
+
"""`explorer`: the static explorer page with one data file per target."""
|
|
180
|
+
# The page shipped with the package unless another is given.
|
|
181
|
+
page_path = f.get("page", str(Path(__file__).parent / "data" / "explorer.html"))
|
|
182
|
+
try:
|
|
183
|
+
page = Path(page_path).read_text(encoding="utf-8")
|
|
184
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
185
|
+
raise Failure(f"{page_path}: {exc}") from exc
|
|
186
|
+
out = Path(need(f, "out"))
|
|
187
|
+
targets = []
|
|
188
|
+
for spec in need(f, "targets").split(","):
|
|
189
|
+
name, eq, d = spec.partition("=")
|
|
190
|
+
if not eq:
|
|
191
|
+
raise Failure(f"--targets entry '{spec}' must be NAME=DIR")
|
|
192
|
+
targets.append((name, read_json(Path(d) / "bundle.json"), read_json(Path(d) / "receipt.json")))
|
|
193
|
+
try:
|
|
194
|
+
page, files, total = explorer.assemble(page, targets)
|
|
195
|
+
except explorer.ExplorerError as exc:
|
|
196
|
+
raise Failure(str(exc)) from exc
|
|
197
|
+
(out / "data").mkdir(parents=True, exist_ok=True)
|
|
198
|
+
write(out / "index.html", page)
|
|
199
|
+
outputs = {"index.html": sha256(page)}
|
|
200
|
+
for path, content in files:
|
|
201
|
+
write(out / path, content)
|
|
202
|
+
outputs[path] = sha256(content)
|
|
203
|
+
print(f"explorer: {len(targets)} targets, {total} bytes (gate {explorer.GATE_BYTES}), size gate PASS", file=sys.stderr)
|
|
204
|
+
emit({"total_bytes": total, "gate_bytes": explorer.GATE_BYTES, "size_gate": "PASS", "outputs": outputs,
|
|
205
|
+
"bug-age": __version__}, out / "receipt.json")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def cmd_mcp(f):
|
|
209
|
+
"""`mcp`: read-only MCP server on stdio."""
|
|
210
|
+
targets = []
|
|
211
|
+
for spec in need(f, "bundles").split(","):
|
|
212
|
+
name, eq, file = spec.partition("=")
|
|
213
|
+
if not eq:
|
|
214
|
+
raise Failure(f"--bundles entry '{spec}' must be NAME=FILE")
|
|
215
|
+
try:
|
|
216
|
+
targets.append(mcp.Target(name, file))
|
|
217
|
+
except RuntimeError as exc:
|
|
218
|
+
raise Failure(str(exc)) from exc
|
|
219
|
+
print(f"bug-age mcp: serving {len(targets)} target(s) on stdio", file=sys.stderr)
|
|
220
|
+
stdin = open(sys.stdin.fileno(), encoding="utf-8", closefd=False)
|
|
221
|
+
stdout = open(sys.stdout.fileno(), "w", encoding="utf-8", newline="\n", closefd=False)
|
|
222
|
+
mcp.serve(targets, stdin, stdout)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def cmd_sweep(f):
|
|
226
|
+
"""`sweep`: Method B for one fix (see bug_age.sweep)."""
|
|
227
|
+
from .sweep import cmd_sweep as run
|
|
228
|
+
run(f)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def cmd_probe(f):
|
|
232
|
+
"""`probe`: test given commits with a regression overlay (see bug_age.sweep)."""
|
|
233
|
+
from .sweep import cmd_probe as run
|
|
234
|
+
run(f)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
COMMANDS = {"catalog": cmd_catalog, "szz": cmd_szz, "sweep": cmd_sweep, "probe": cmd_probe,
|
|
238
|
+
"bundle": cmd_bundle, "explorer": cmd_explorer, "mcp": cmd_mcp}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def main(argv=None):
|
|
242
|
+
"""Run one subcommand; return the exit status."""
|
|
243
|
+
args = sys.argv[1:] if argv is None else argv
|
|
244
|
+
first = args[0] if args else None
|
|
245
|
+
if first in ("--version", "-V"):
|
|
246
|
+
print(f"bug-age {__version__}")
|
|
247
|
+
return 0
|
|
248
|
+
if first in (None, "--help", "-h"):
|
|
249
|
+
print(USAGE)
|
|
250
|
+
return 0
|
|
251
|
+
if first == "pipeline":
|
|
252
|
+
from .pipeline import main as pipeline
|
|
253
|
+
return pipeline(args[1:])
|
|
254
|
+
if first == "dockerfile":
|
|
255
|
+
sys.stdout.write((Path(__file__).parent / "data" / "Dockerfile").read_text(encoding="utf-8"))
|
|
256
|
+
return 0
|
|
257
|
+
if first not in COMMANDS:
|
|
258
|
+
print(f"bug-age: unknown argument '{first}'\n{USAGE}", file=sys.stderr)
|
|
259
|
+
return 2
|
|
260
|
+
try:
|
|
261
|
+
COMMANDS[first](flags(args[1:]))
|
|
262
|
+
except (Failure, RuntimeError) as exc:
|
|
263
|
+
print(f"bug-age: {exc}", file=sys.stderr)
|
|
264
|
+
return 1
|
|
265
|
+
return 0
|
bug_age/data/Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Per-commit build environment for bug-age (spike and `bug-age sweep --runner docker`).
|
|
2
|
+
# rustup and its cargo/rustc proxies live in the image; toolchains (RUSTUP_HOME=/rustup) and the
|
|
3
|
+
# cargo registry (CARGO_HOME=/cargo) live in named volumes so they persist across commits. Before
|
|
4
|
+
# v4, rustup itself lived in the /cargo volume, so deleting that volume broke the image.
|
|
5
|
+
# safe.directory: index repos are staged from the host; ownership must not block cargo's libgit2.
|
|
6
|
+
FROM debian:bookworm-slim
|
|
7
|
+
RUN apt-get update \
|
|
8
|
+
&& apt-get install -y --no-install-recommends ca-certificates curl gcc g++ libc6-dev make cmake pkg-config git clang libclang-dev libssl-dev \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
10
|
+
RUN curl -sSf https://sh.rustup.rs | CARGO_HOME=/opt/rustup-bin RUSTUP_HOME=/tmp/rustup-init \
|
|
11
|
+
sh -s -- -y --profile minimal --default-toolchain none --no-modify-path \
|
|
12
|
+
&& rm -rf /tmp/rustup-init
|
|
13
|
+
RUN git config --system --add safe.directory "*"
|
|
14
|
+
ENV RUSTUP_HOME=/rustup CARGO_HOME=/cargo PATH=/opt/rustup-bin/bin:$PATH
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://bug-age.invalid/schema/bundle-1.0.0.json",
|
|
4
|
+
"title": "bug-age bundle",
|
|
5
|
+
"description": "Canonical data for one target repository. Every number is produced by bug-age scripts; nothing is hand-entered. Blameless: no names or emails (enforced by the build gate).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schema_version", "generator", "subject", "summary", "catalog", "method_a", "method_b", "feasibility", "coverage", "commits"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"$defs": {
|
|
10
|
+
"sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"},
|
|
11
|
+
"sha_or_null": {"anyOf": [{"$ref": "#/$defs/sha"}, {"type": "null"}]},
|
|
12
|
+
"stat": {
|
|
13
|
+
"description": "Distribution over bugs: median, 90th percentile, maximum, sample size.",
|
|
14
|
+
"anyOf": [{"type": "null"}, {"type": "object", "required": ["median", "p90", "max", "n"],
|
|
15
|
+
"properties": {"median": {"type": "number"}, "p90": {"type": "number"}, "max": {"type": "number"}, "n": {"type": "integer", "minimum": 0}}}]
|
|
16
|
+
},
|
|
17
|
+
"outcome": {"enum": ["PASS", "FAIL", "UNTESTABLE", "FLAKY"]},
|
|
18
|
+
"feasibility_run": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["verdict", "built_and_tested_share", "counts", "method", "samples"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"verdict": {"enum": ["GO", "NO-GO"]},
|
|
23
|
+
"built_and_tested_share": {"type": "number", "minimum": 0, "maximum": 1},
|
|
24
|
+
"counts": {"type": "object", "additionalProperties": {"type": "integer"}},
|
|
25
|
+
"method": {"type": "object"},
|
|
26
|
+
"samples": {"type": "array", "items": {"type": "object", "required": ["sha", "date", "toolchain", "status", "reason", "lockfile_committed"],
|
|
27
|
+
"properties": {"sha": {"$ref": "#/$defs/sha"}, "status": {"enum": ["BUILT_AND_TESTED", "BUILD_FAILED", "TESTS_FAILED", "NOT_TESTED", "FLAKY"]}}}}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"properties": {
|
|
32
|
+
"schema_version": {"const": "1.1.0"},
|
|
33
|
+
"generator": {"type": "string", "description": "bug-age version that built the bundle"},
|
|
34
|
+
"subject": {"type": "object", "required": ["repo_url", "crate_path", "head"],
|
|
35
|
+
"properties": {"repo_url": {"type": "string"}, "crate_path": {"type": "string"}, "head": {"$ref": "#/$defs/sha"}}},
|
|
36
|
+
"summary": {
|
|
37
|
+
"description": "Headline metrics. The explorer page recomputes them from the rows below as a self-check.",
|
|
38
|
+
"type": "object", "required": ["method_a", "method_b", "feasibility"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"method_a": {"type": "object", "required": ["fixes", "with_introducer", "calendar_days", "commits_elapsed", "releases_shipped_bug"],
|
|
41
|
+
"properties": {"fixes": {"type": "integer"}, "with_introducer": {"type": "integer"},
|
|
42
|
+
"calendar_days": {"$ref": "#/$defs/stat"}, "commits_elapsed": {"$ref": "#/$defs/stat"}, "releases_shipped_bug": {"$ref": "#/$defs/stat"}}},
|
|
43
|
+
"method_b": {"type": "object", "required": ["swept", "located", "a_refuted", "verdicts"]},
|
|
44
|
+
"feasibility": {"type": "object", "required": ["baseline_share", "time_travel_share"]}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"catalog": {"type": "object", "required": ["include_rule", "scanned", "excluded"],
|
|
48
|
+
"properties": {"excluded": {"type": "array", "items": {"type": "object", "required": ["sha", "rule"],
|
|
49
|
+
"properties": {"sha": {"$ref": "#/$defs/sha"}, "rule": {"type": "string", "pattern": "^exclude:"}}}}}},
|
|
50
|
+
"method_a": {
|
|
51
|
+
"description": "One entry per catalogued fix (SZZ-style). days = committer days, or author days when the flag committer-dates-compressed is set.",
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": {"type": "object", "required": ["fix", "fix_date", "nominees", "flags", "days"],
|
|
54
|
+
"not": {"required": ["subject"]},
|
|
55
|
+
"properties": {"fix": {"$ref": "#/$defs/sha"}, "days": {"type": ["number", "null"]},
|
|
56
|
+
"nominees": {"type": "array", "items": {"type": "object", "required": ["sha", "date", "blamed_lines"], "properties": {"sha": {"$ref": "#/$defs/sha"}}}},
|
|
57
|
+
"flags": {"type": "array", "items": {"type": "string"}}}}
|
|
58
|
+
},
|
|
59
|
+
"method_b": {
|
|
60
|
+
"description": "One entry per behavioural sweep. days is exact only when verdict is located; otherwise a lower bound (days_is_lower_bound).",
|
|
61
|
+
"type": "array",
|
|
62
|
+
"items": {"type": "object",
|
|
63
|
+
"required": ["id", "fix", "verdict", "first_failing", "last_passing", "days", "days_is_lower_bound", "method_a_nominee", "a_check", "results", "repro_test"],
|
|
64
|
+
"properties": {
|
|
65
|
+
"fix": {"$ref": "#/$defs/sha"},
|
|
66
|
+
"verdict": {"enum": ["located", "interval", "censored-by-testability", "left-censored", "repro-invalid"]},
|
|
67
|
+
"first_failing": {"$ref": "#/$defs/sha_or_null"},
|
|
68
|
+
"last_passing": {"$ref": "#/$defs/sha_or_null"},
|
|
69
|
+
"days_is_lower_bound": {"type": "boolean"},
|
|
70
|
+
"a_check": {"type": "object", "required": ["label", "outcomes"],
|
|
71
|
+
"properties": {"label": {"enum": ["confirmed", "too-late", "refuted", "plausible", "untested", "inconclusive", "not-run"]}}},
|
|
72
|
+
"results": {"type": "array", "items": {"type": "object", "required": ["sha", "outcome", "steps"],
|
|
73
|
+
"properties": {"sha": {"$ref": "#/$defs/sha"}, "outcome": {"$ref": "#/$defs/outcome"}}}}
|
|
74
|
+
}}
|
|
75
|
+
},
|
|
76
|
+
"feasibility": {"type": "object", "required": ["baseline", "time_travel"],
|
|
77
|
+
"description": "null = that feasibility run did not happen for this target",
|
|
78
|
+
"properties": {"baseline": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/feasibility_run"}]}, "time_travel": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/feasibility_run"}]}}},
|
|
79
|
+
"coverage": {"type": "array", "items": {"type": "object", "required": ["stage", "status", "evidence", "proves", "does_not_prove"],
|
|
80
|
+
"properties": {"status": {"enum": ["COVERED", "PARTIAL", "NOT TESTED"]}}}},
|
|
81
|
+
"commits": {
|
|
82
|
+
"description": "Every SHA referenced anywhere in the bundle: committer date and a short, redacted subject. No author or committer identity.",
|
|
83
|
+
"type": "object",
|
|
84
|
+
"propertyNames": {"pattern": "^[0-9a-f]{40}$"},
|
|
85
|
+
"additionalProperties": {"type": "object", "required": ["date", "subject"], "additionalProperties": false,
|
|
86
|
+
"properties": {"date": {"type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$"}, "subject": {"type": "string", "maxLength": 100}}}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|